Refactor chore management modals and actions for centralized handling
- Removed individual modal states and handlers from ChoreCard and CompactChoreCard components. - Introduced a centralized modal state and handler in MyChores component to manage modals for changing due dates, completing with past dates, changing assignees, adding notes, writing NFC, and nudging. - Updated action handlers to utilize the new centralized approach for better maintainability and readability. - Cleaned up unused imports and code related to modal management in ChoreCard and CompactChoreCard components.
This commit is contained in:
@@ -78,6 +78,9 @@ const BottomSheetModal = forwardRef(
|
||||
// Calculate current height
|
||||
const currentHeight = isExpanded ? expandedHeight : height
|
||||
|
||||
// Filter out DOM props that shouldn't be passed to Modal
|
||||
const { fullWidth: _fullWidth, unmountDelay: _unmountDelay, ...modalProps } = props
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={internalOpen}
|
||||
@@ -92,7 +95,7 @@ const BottomSheetModal = forwardRef(
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
keepMounted
|
||||
{...props}
|
||||
{...modalProps}
|
||||
>
|
||||
<Sheet
|
||||
ref={ref}
|
||||
|
||||
@@ -14,6 +14,8 @@ const FadeModal = ({
|
||||
backdropBlur = true,
|
||||
...props
|
||||
}) => {
|
||||
// Filter out props that shouldn't be passed to Modal
|
||||
const { unmountDelay: _unmountDelay, ...modalProps } = props
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -34,7 +36,7 @@ const FadeModal = ({
|
||||
exit: 'cubic-bezier(0.4, 0, 0.2, 1)', // Standard ease out
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
{...modalProps}
|
||||
>
|
||||
<ModalOverflow>
|
||||
<ModalDialog
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ApproveChore,
|
||||
ArchiveChore,
|
||||
CreateChore,
|
||||
DeleteChore,
|
||||
DeleteChoreHistory,
|
||||
GetChoreByID,
|
||||
GetChoreDetailById,
|
||||
@@ -66,41 +67,106 @@ export const useChores = includeArchive => {
|
||||
},
|
||||
})
|
||||
}
|
||||
export const useDeleteChores = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async choreIds => {
|
||||
// If offline mode is enabled and we're offline, handle deletion locally
|
||||
if (!networkManager.isOnline && isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
const offlineTasks =
|
||||
(await localStore.getFromCache('offlineTasks')) || []
|
||||
const updatedOfflineTasks = offlineTasks.filter(
|
||||
task =>
|
||||
!choreIds.includes(task.id) && !choreIds.includes(task.tempId),
|
||||
)
|
||||
await localStore.saveToCache('offlineTasks', updatedOfflineTasks)
|
||||
// Force the chores query to refetch
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
return
|
||||
}
|
||||
|
||||
// If online, proceed with server-side deletion
|
||||
await Promise.all(
|
||||
choreIds.map(async id => {
|
||||
const resp = await DeleteChore(id)
|
||||
if (!resp || !resp.ok) {
|
||||
throw new Error(`Failed to delete chore with ID: ${id}`)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
},
|
||||
})
|
||||
}
|
||||
export const useCreateChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: CreateChore,
|
||||
onMutate: async newTask => {
|
||||
if (!networkManager.isOnline && isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
const tempId = crypto.randomUUID() // Generate temp ID
|
||||
const offlineTasks =
|
||||
(await localStore.getFromCache('offlineTasks')) || []
|
||||
const updateOfflineTasks = [
|
||||
...offlineTasks,
|
||||
{ ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking
|
||||
]
|
||||
await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage
|
||||
// force useChores to refetch:
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
// Force the chores query to refetch
|
||||
queryClient.refetchQueries(['chores'])
|
||||
// Update the chores query cache immediately
|
||||
// queryClient.setQueryData(['chores'], oldData => {
|
||||
// console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
||||
|
||||
// if (!oldData)
|
||||
// return {
|
||||
// res: [{ ...newTask, id: tempId, tempId }],
|
||||
// } // If no data, return offline tasks
|
||||
// return {
|
||||
// res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||
// }
|
||||
// })
|
||||
return { tempId }
|
||||
mutationFn: async newTask => {
|
||||
const resp = await CreateChore(newTask)
|
||||
if (!resp || !resp.ok) {
|
||||
throw new Error('Failed to create chore')
|
||||
}
|
||||
return { tempId: null }
|
||||
const createdChore = await resp.json()
|
||||
if (!createdChore) {
|
||||
throw new Error('Failed to get created chore data')
|
||||
}
|
||||
// Successfully created the chore on the server, return the created chore
|
||||
// update the local chores cache with the new chore:
|
||||
queryClient.setQueryData(['chores'], oldData => {
|
||||
if (!oldData) return { res: [createdChore.res] }
|
||||
return { res: [...oldData.res, createdChore.res] }
|
||||
})
|
||||
return { res: createdChore }
|
||||
},
|
||||
|
||||
// onMutate: async newTask => {
|
||||
// if (!networkManager.isOnline && isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
// const tempId = crypto.randomUUID() // Generate temp ID
|
||||
// const offlineTasks =
|
||||
// (await localStore.getFromCache('offlineTasks')) || []
|
||||
// const updateOfflineTasks = [
|
||||
// ...offlineTasks,
|
||||
// { ...newTask, id: tempId, tempId }, // Use the tempId for offline tracking
|
||||
// ]
|
||||
// await localStore.saveToCache('offlineTasks', updateOfflineTasks) // Save to local storage
|
||||
// // force useChores to refetch:
|
||||
// queryClient.invalidateQueries(['chores'])
|
||||
// // Force the chores query to refetch
|
||||
// queryClient.refetchQueries(['chores'])
|
||||
// // Update the chores query cache immediately
|
||||
// // queryClient.setQueryData(['chores'], oldData => {
|
||||
// // console.log('ATTEMPT TO SAVE OFFLINE TASKS:', updateOfflineTasks)
|
||||
|
||||
// // if (!oldData)
|
||||
// // return {
|
||||
// // res: [{ ...newTask, id: tempId, tempId }],
|
||||
// // } // If no data, return offline tasks
|
||||
// // return {
|
||||
// // res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||
// // }
|
||||
// // })
|
||||
// return { tempId }
|
||||
// }
|
||||
// const tempId = crypto.randomUUID() // Generate temp ID
|
||||
// // Update the chores query cache immediately
|
||||
// queryClient.setQueryData(['chores'], oldData => {
|
||||
// if (!oldData)
|
||||
// return {
|
||||
// res: [{ ...newTask, id: tempId, tempId }],
|
||||
// } // If no data, return offline tasks
|
||||
// return {
|
||||
// res: [...oldData.res, { ...newTask, id: tempId, tempId }],
|
||||
// }
|
||||
// })
|
||||
// return { tempId: null }
|
||||
// },
|
||||
onSuccess: () => {
|
||||
// Invalidate the chores query to refresh the data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -157,6 +223,15 @@ export const useUpdateChore = () => {
|
||||
throw new Error('Failed to get updated chore data')
|
||||
}
|
||||
// Successfully updated the chore on the server, return the updated chore
|
||||
// update the local chores cache with the updated chore:
|
||||
queryClient.setQueryData(['chores'], oldData => {
|
||||
if (!oldData) return { res: [updatedChore] }
|
||||
return {
|
||||
res: oldData.res.map(chore =>
|
||||
chore.id === updatedChore.id ? updatedChore : chore,
|
||||
),
|
||||
}
|
||||
})
|
||||
return updatedChoreRes?.res || updatedChoreRes
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,8 +26,10 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import NotificationTemplate from '../../components/NotificationTemplate.jsx'
|
||||
import {
|
||||
useArchiveChore,
|
||||
useChore,
|
||||
useCreateChore,
|
||||
useUnArchiveChore,
|
||||
useUpdateChore,
|
||||
} from '../../queries/ChoreQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
@@ -56,6 +58,7 @@ const ASSIGN_STRATEGIES = [
|
||||
'keep_last_assigned',
|
||||
'random_except_last_assigned',
|
||||
'round_robin',
|
||||
'no_assignee',
|
||||
]
|
||||
const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
|
||||
|
||||
@@ -111,6 +114,8 @@ const ChoreEdit = () => {
|
||||
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
|
||||
const updateChoreMutation = useUpdateChore()
|
||||
const createChoreMutation = useCreateChore()
|
||||
const archiveChore = useArchiveChore()
|
||||
const unarchiveChore = useUnArchiveChore()
|
||||
const {
|
||||
data: choreData,
|
||||
isLoading: isChoreLoading,
|
||||
@@ -136,11 +141,13 @@ const ChoreEdit = () => {
|
||||
if (name.trim() === '') {
|
||||
errors.name = 'Name is required'
|
||||
}
|
||||
if (assignees.length === 0) {
|
||||
errors.assignees = 'At least 1 assignees is required'
|
||||
}
|
||||
if (assignedTo < 0) {
|
||||
errors.assignedTo = 'Assigned to is required'
|
||||
if (assignStrategy !== 'no_assignee') {
|
||||
if (assignees.length === 0) {
|
||||
errors.assignees = 'At least 1 assignees is required'
|
||||
}
|
||||
if (assignedTo === null || assignedTo < 0) {
|
||||
errors.assignedTo = 'Assigned to is required'
|
||||
}
|
||||
}
|
||||
if (frequencyType === 'interval' && !frequency > 0) {
|
||||
errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0`
|
||||
@@ -228,7 +235,7 @@ const ChoreEdit = () => {
|
||||
frequencyType: frequencyType,
|
||||
frequency: Number(frequency),
|
||||
frequencyMetadata: frequencyMetadata,
|
||||
assignedTo: assignedTo,
|
||||
assignedTo: assignStrategy === 'no_assignee' ? null : assignedTo,
|
||||
assignStrategy: assignStrategy,
|
||||
isRolling: isRolling,
|
||||
isActive: isActive,
|
||||
@@ -379,20 +386,26 @@ const ChoreEdit = () => {
|
||||
}, [frequencyType])
|
||||
|
||||
useEffect(() => {
|
||||
if (assignees.length === 1) {
|
||||
if (assignees.length === 0) {
|
||||
setAssignStrategy('no_assignee')
|
||||
setAssignedTo(null)
|
||||
} else if (assignees.length === 1) {
|
||||
setAssignedTo(assignees[0].userId)
|
||||
if (assignStrategy === 'no_assignee') {
|
||||
setAssignStrategy(ASSIGN_STRATEGIES[2]) // default to least_completed
|
||||
}
|
||||
}
|
||||
}, [assignees])
|
||||
}, [assignees, assignStrategy])
|
||||
|
||||
useEffect(() => {
|
||||
if (performers.length > 0 && assignees.length === 0 && userProfile) {
|
||||
setAssignees([
|
||||
{
|
||||
userId: userProfile?.id,
|
||||
},
|
||||
])
|
||||
}
|
||||
}, [performers, userProfile])
|
||||
// useEffect(() => {
|
||||
// if (performers.length > 0 && assignees.length === 0 && userProfile) {
|
||||
// setAssignees([
|
||||
// {
|
||||
// userId: userProfile?.id,
|
||||
// },
|
||||
// ])
|
||||
// }
|
||||
// }, [performers, userProfile])
|
||||
|
||||
// if user resolve the error trigger validation to remove the error message from the respective field
|
||||
useEffect(() => {
|
||||
@@ -1221,16 +1234,39 @@ const ChoreEdit = () => {
|
||||
}}
|
||||
>
|
||||
{choreId > 0 && (
|
||||
<Button
|
||||
color='danger'
|
||||
variant='solid'
|
||||
onClick={() => {
|
||||
// confirm before deleting:
|
||||
handleDelete()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<>
|
||||
{isActive ? (
|
||||
<Button
|
||||
color='danger'
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
archiveChore.mutate(choreId)
|
||||
}}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color='neutral'
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
unarchiveChore.mutate(choreId)
|
||||
}}
|
||||
>
|
||||
Unarchive
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color='danger'
|
||||
variant='solid'
|
||||
onClick={() => {
|
||||
// confirm before deleting:
|
||||
handleDelete()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
color='neutral'
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { config } from 'dotenv'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -42,18 +43,10 @@ import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import DateModal from '../Modals/Inputs/DateModal'
|
||||
import NudgeModal from '../Modals/Inputs/NudgeModal'
|
||||
import SelectModal from '../Modals/Inputs/SelectModal'
|
||||
import TextModal from '../Modals/Inputs/TextModal'
|
||||
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
||||
import ChoreActionMenu from '../components/ChoreActionMenu'
|
||||
const ChoreCard = ({
|
||||
chore,
|
||||
@@ -63,22 +56,13 @@ const ChoreCard = ({
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
onAction,
|
||||
// Multi-select props
|
||||
isMultiSelectMode = false,
|
||||
isSelected = false,
|
||||
onSelectionToggle,
|
||||
}) => {
|
||||
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
|
||||
React.useState(false)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
|
||||
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
|
||||
const [isNudgeModalOpen, setIsNudgeModalOpen] = React.useState(false)
|
||||
const [isOfficialInstance, setIsOfficialInstance] = React.useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -200,59 +184,6 @@ const ChoreCard = ({
|
||||
setTimeoutId(id)
|
||||
}
|
||||
|
||||
const handleChangeDueDate = newDate => {
|
||||
UpdateDueDate(chore.id, newDate).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'rescheduled')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompleteWithPastDate = newDate => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
new Date(newDate).toISOString(),
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleAssigneChange = assigneeId => {
|
||||
UpdateChoreAssignee(chore.id, assigneeId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'assigned')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleCompleteWithNote = note => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser
|
||||
? { note, completedBy: impersonatedUser.userId }
|
||||
: { note },
|
||||
null,
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
@@ -276,32 +207,6 @@ const ChoreCard = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleNudge = async ({ choreId, message, notifyAllAssignees }) => {
|
||||
try {
|
||||
const response = await NudgeChore(choreId, {
|
||||
message,
|
||||
notifyAllAssignees,
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
showNotification({
|
||||
type: 'success',
|
||||
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 {
|
||||
setIsNudgeModalOpen(false)
|
||||
resetSwipe()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
@@ -807,7 +712,7 @@ const ChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
setIsChangeDueDateModalOpen(true)
|
||||
onAction('changeDueDate', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -844,7 +749,7 @@ const ChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
setIsNudgeModalOpen(true)
|
||||
onAction('nudge', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -1241,16 +1146,12 @@ const ChoreCard = ({
|
||||
chore={chore}
|
||||
onChoreUpdate={onChoreUpdate}
|
||||
onChoreRemove={onChoreRemove}
|
||||
onCompleteWithNote={() =>
|
||||
setIsCompleteWithNoteModalOpen(true)
|
||||
}
|
||||
onCompleteWithPastDate={() =>
|
||||
setIsCompleteWithPastDateModalOpen(true)
|
||||
}
|
||||
onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
|
||||
onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
|
||||
onWriteNFC={() => setIsNFCModalOpen(true)}
|
||||
onNudge={() => setIsNudgeModalOpen(true)}
|
||||
onCompleteWithNote={() => onAction('completeWithNote', chore)}
|
||||
onCompleteWithPastDate={() => onAction('completeWithPastDate', chore)}
|
||||
onChangeAssignee={() => onAction('changeAssignee', chore)}
|
||||
onChangeDueDate={() => onAction('changeDueDate', chore)}
|
||||
onWriteNFC={() => onAction('writeNFC', chore)}
|
||||
onNudge={() => onAction('nudge', chore)}
|
||||
onDelete={handleDelete}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onOpen={() => {
|
||||
@@ -1264,69 +1165,9 @@ const ChoreCard = ({
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<DateModal
|
||||
isOpen={isChangeDueDateModalOpen}
|
||||
key={'changeDueDate' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Change due date`}
|
||||
onClose={() => {
|
||||
setIsChangeDueDateModalOpen(false)
|
||||
}}
|
||||
onSave={handleChangeDueDate}
|
||||
/>
|
||||
<DateModal
|
||||
isOpen={isCompleteWithPastDateModalOpen}
|
||||
key={'completedInPast' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Save Chore that you completed in the past`}
|
||||
onClose={() => {
|
||||
setIsCompleteWithPastDateModalOpen(false)
|
||||
}}
|
||||
onSave={handleCompleteWithPastDate}
|
||||
/>
|
||||
<SelectModal
|
||||
isOpen={isChangeAssigneeModalOpen}
|
||||
options={performers}
|
||||
displayKey='displayName'
|
||||
title={`Delegate to someone else`}
|
||||
placeholder={'Select a performer'}
|
||||
onClose={() => {
|
||||
setIsChangeAssigneeModalOpen(false)
|
||||
}}
|
||||
onSave={selected => {
|
||||
handleAssigneChange(selected.id)
|
||||
}}
|
||||
/>
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
<TextModal
|
||||
isOpen={isCompleteWithNoteModalOpen}
|
||||
title='Add note to attach to this completion:'
|
||||
onClose={() => {
|
||||
setIsCompleteWithNoteModalOpen(false)
|
||||
}}
|
||||
okText={'Complete'}
|
||||
onSave={handleCompleteWithNote}
|
||||
/>
|
||||
<WriteNFCModal
|
||||
config={{
|
||||
isOpen: isNFCModalOpen,
|
||||
url: `${window.location.origin}/chores/${chore.id}`,
|
||||
onClose: () => {
|
||||
setIsNFCModalOpen(false)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<NudgeModal
|
||||
config={{
|
||||
isOpen: isNudgeModalOpen,
|
||||
choreId: chore.id,
|
||||
onClose: () => setIsNudgeModalOpen(false),
|
||||
onConfirm: handleNudge,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
<Snackbar
|
||||
|
||||
@@ -40,18 +40,10 @@ import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import { usePauseChore, useStartChore } from '../../queries/TimeQueries'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import DateModal from '../Modals/Inputs/DateModal'
|
||||
import NudgeModal from '../Modals/Inputs/NudgeModal'
|
||||
import SelectModal from '../Modals/Inputs/SelectModal'
|
||||
import TextModal from '../Modals/Inputs/TextModal'
|
||||
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
||||
import ChoreActionMenu from '../components/ChoreActionMenu'
|
||||
|
||||
const CompactChoreCard = ({
|
||||
@@ -62,22 +54,13 @@ const CompactChoreCard = ({
|
||||
sx,
|
||||
viewOnly,
|
||||
onChipClick,
|
||||
onAction,
|
||||
// Multi-select props
|
||||
isMultiSelectMode = false,
|
||||
isSelected = false,
|
||||
onSelectionToggle,
|
||||
}) => {
|
||||
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
|
||||
React.useState(false)
|
||||
const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] =
|
||||
React.useState(false)
|
||||
const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
|
||||
React.useState(false)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
|
||||
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
|
||||
const [isNudgeModalOpen, setIsNudgeModalOpen] = React.useState(false)
|
||||
const [isOfficialInstance, setIsOfficialInstance] = React.useState(false)
|
||||
const navigate = useNavigate()
|
||||
const startChore = useStartChore()
|
||||
@@ -91,7 +74,7 @@ const CompactChoreCard = ({
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError, showNotification } = useNotification()
|
||||
const { showError } = useNotification()
|
||||
|
||||
// Swipe functionality state
|
||||
const [swipeTranslateX, setSwipeTranslateX] = React.useState(0)
|
||||
@@ -372,61 +355,6 @@ const CompactChoreCard = ({
|
||||
setTimeoutId(id)
|
||||
}
|
||||
|
||||
const handleChangeDueDate = newDate => {
|
||||
UpdateDueDate(chore.id, newDate).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'rescheduled')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompleteWithPastDate = newDate => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
|
||||
new Date(newDate).toISOString(),
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAssigneChange = assigneeId => {
|
||||
UpdateChoreAssignee(chore.id, assigneeId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'assigned')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCompleteWithNote = note => {
|
||||
MarkChoreComplete(
|
||||
chore.id,
|
||||
impersonatedUser
|
||||
? { note, completedBy: impersonatedUser.userId }
|
||||
: { note },
|
||||
null,
|
||||
null,
|
||||
).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
onChoreUpdate(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
@@ -450,32 +378,6 @@ const CompactChoreCard = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleNudge = async ({ choreId, message, notifyAllAssignees }) => {
|
||||
try {
|
||||
const response = await NudgeChore(choreId, {
|
||||
message,
|
||||
notifyAllAssignees,
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
showNotification({
|
||||
type: 'success',
|
||||
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 {
|
||||
setIsNudgeModalOpen(false)
|
||||
resetSwipe()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
@@ -794,7 +696,7 @@ const CompactChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
setIsChangeDueDateModalOpen(true)
|
||||
onAction('changeDueDate', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -841,7 +743,7 @@ const CompactChoreCard = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
setIsNudgeModalOpen(true)
|
||||
onAction('nudge', chore)
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
@@ -1271,14 +1173,12 @@ const CompactChoreCard = ({
|
||||
chore={chore}
|
||||
onChoreUpdate={onChoreUpdate}
|
||||
onChoreRemove={onChoreRemove}
|
||||
onCompleteWithNote={() => setIsCompleteWithNoteModalOpen(true)}
|
||||
onCompleteWithPastDate={() =>
|
||||
setIsCompleteWithPastDateModalOpen(true)
|
||||
}
|
||||
onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
|
||||
onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
|
||||
onWriteNFC={() => setIsNFCModalOpen(true)}
|
||||
onNudge={() => setIsNudgeModalOpen(true)}
|
||||
onCompleteWithNote={() => onAction('completeWithNote', chore)}
|
||||
onCompleteWithPastDate={() => onAction('completeWithPastDate', chore)}
|
||||
onChangeAssignee={() => onAction('changeAssignee', chore)}
|
||||
onChangeDueDate={() => onAction('changeDueDate', chore)}
|
||||
onWriteNFC={() => onAction('writeNFC', chore)}
|
||||
onNudge={() => onAction('nudge', chore)}
|
||||
onDelete={handleDelete}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
// onMouseLeave={handleMouseLeave}
|
||||
@@ -1300,64 +1200,10 @@ const CompactChoreCard = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* All modals (same as original) */}
|
||||
<DateModal
|
||||
isOpen={isChangeDueDateModalOpen}
|
||||
key={'changeDueDate' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Change due date`}
|
||||
onClose={() => setIsChangeDueDateModalOpen(false)}
|
||||
onSave={handleChangeDueDate}
|
||||
/>
|
||||
|
||||
<DateModal
|
||||
isOpen={isCompleteWithPastDateModalOpen}
|
||||
key={'completedInPast' + chore.id}
|
||||
current={chore.nextDueDate}
|
||||
title={`Save Chore that you completed in the past`}
|
||||
onClose={() => setIsCompleteWithPastDateModalOpen(false)}
|
||||
onSave={handleCompleteWithPastDate}
|
||||
/>
|
||||
|
||||
<SelectModal
|
||||
isOpen={isChangeAssigneeModalOpen}
|
||||
options={performers}
|
||||
displayKey='displayName'
|
||||
title={`Delegate to someone else`}
|
||||
placeholder={'Select a performer'}
|
||||
onClose={() => setIsChangeAssigneeModalOpen(false)}
|
||||
onSave={selected => handleAssigneChange(selected.id)}
|
||||
/>
|
||||
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
|
||||
<TextModal
|
||||
isOpen={isCompleteWithNoteModalOpen}
|
||||
title='Add note to attach to this completion:'
|
||||
onClose={() => setIsCompleteWithNoteModalOpen(false)}
|
||||
okText={'Complete'}
|
||||
onSave={handleCompleteWithNote}
|
||||
/>
|
||||
|
||||
<WriteNFCModal
|
||||
config={{
|
||||
isOpen: isNFCModalOpen,
|
||||
url: `${window.location.origin}/chores/${chore.id}`,
|
||||
onClose: () => setIsNFCModalOpen(false),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NudgeModal
|
||||
config={{
|
||||
isOpen: isNudgeModalOpen,
|
||||
choreId: chore.id,
|
||||
onClose: () => setIsNudgeModalOpen(false),
|
||||
onConfirm: handleNudge,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Snackbar for pending completion */}
|
||||
<Snackbar
|
||||
open={isPendingCompletion}
|
||||
|
||||
@@ -39,24 +39,41 @@ import {
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { useArchiveChore, useChores } from '../../queries/ChoreQueries'
|
||||
import {
|
||||
useArchiveChore,
|
||||
useChores,
|
||||
useDeleteChores,
|
||||
useUnArchiveChore,
|
||||
} from '../../queries/ChoreQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { TASK_COLOR } from '../../utils/Colors'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import DateModal from '../Modals/Inputs/DateModal'
|
||||
import NudgeModal from '../Modals/Inputs/NudgeModal'
|
||||
import SelectModal from '../Modals/Inputs/SelectModal'
|
||||
import TextModal from '../Modals/Inputs/TextModal'
|
||||
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
|
||||
import ChoreCard from './ChoreCard'
|
||||
import CompactChoreCard from './CompactChoreCard'
|
||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||
import MultiSelectHelp from './MultiSelectHelp'
|
||||
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||
import { DeleteChore, MarkChoreComplete, SkipChore } from '../../utils/Fetcher'
|
||||
import {
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
SkipChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import { getSafeBottom } from '../../utils/SafeAreaUtils.js'
|
||||
import TaskInput from '../components/AddTaskModal'
|
||||
import CalendarDual from '../components/CalendarDual'
|
||||
@@ -74,8 +91,11 @@ const MyChores = () => {
|
||||
useUserProfile()
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||
const { showSuccess, showError, showWarning } = useNotification()
|
||||
const queryClient = useQueryClient()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
const archiveChore = useArchiveChore()
|
||||
const unarchiveChore = useUnArchiveChore()
|
||||
const deleteChores = useDeleteChores()
|
||||
const [chores, setChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [searchFilter, setSearchFilter] = useState('All')
|
||||
@@ -121,12 +141,14 @@ const MyChores = () => {
|
||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||
const [selectedChores, setSelectedChores] = useState(new Set())
|
||||
const [confirmModelConfig, setConfirmModelConfig] = useState({})
|
||||
|
||||
// Centralized modal state
|
||||
const [activeModal, setActiveModal] = useState(null)
|
||||
const [modalData, setModalData] = useState({})
|
||||
const [modalChore, setModalChore] = useState(null)
|
||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||
const processedChores = useMemo(() => {
|
||||
console.time('🏁 processedChores calculation')
|
||||
|
||||
if (!choresData?.res) {
|
||||
console.timeEnd('🏁 processedChores calculation')
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -141,15 +163,11 @@ const MyChores = () => {
|
||||
)
|
||||
}
|
||||
|
||||
console.timeEnd('🏁 processedChores calculation')
|
||||
return sortedChores
|
||||
}, [choresData?.res, impersonatedUser])
|
||||
|
||||
const processedSections = useMemo(() => {
|
||||
console.time('🏁 processedSections calculation')
|
||||
|
||||
if (!processedChores.length || !userProfile?.id) {
|
||||
console.timeEnd('🏁 processedSections calculation')
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -161,7 +179,6 @@ const MyChores = () => {
|
||||
],
|
||||
)
|
||||
|
||||
console.timeEnd('🏁 processedSections calculation')
|
||||
return sections
|
||||
}, [
|
||||
processedChores,
|
||||
@@ -179,8 +196,6 @@ const MyChores = () => {
|
||||
membersData?.res &&
|
||||
choresData?.res
|
||||
) {
|
||||
console.time('🏁 Main useEffect processing')
|
||||
|
||||
const processEffectAsync = async () => {
|
||||
setPerformers(membersData.res)
|
||||
setChores(processedChores)
|
||||
@@ -216,8 +231,6 @@ const MyChores = () => {
|
||||
membersData.res,
|
||||
)
|
||||
}
|
||||
|
||||
console.timeEnd('🏁 Main useEffect processing')
|
||||
}
|
||||
|
||||
processEffectAsync()
|
||||
@@ -236,7 +249,6 @@ const MyChores = () => {
|
||||
// Auto-update sections when processedSections changes
|
||||
useEffect(() => {
|
||||
if (processedSections.length > 0) {
|
||||
console.time('🏁 Auto-update sections')
|
||||
setChoreSections(processedSections)
|
||||
|
||||
// Auto-open sections if needed - only check localStorage once
|
||||
@@ -251,7 +263,6 @@ const MyChores = () => {
|
||||
)
|
||||
setOpenChoreSections(openSections)
|
||||
}
|
||||
console.timeEnd('🏁 Auto-update sections')
|
||||
}
|
||||
}, [processedSections])
|
||||
|
||||
@@ -484,6 +495,108 @@ const MyChores = () => {
|
||||
}
|
||||
}, [isMultiSelectMode, selectedChores.size, addTaskModalOpen])
|
||||
|
||||
// Centralized modal handlers
|
||||
const handleChoreAction = (action, chore, extraData = {}) => {
|
||||
setModalChore(chore)
|
||||
setModalData(extraData)
|
||||
setActiveModal(action)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setActiveModal(null)
|
||||
setModalChore(null)
|
||||
setModalData({})
|
||||
}
|
||||
|
||||
const handleChangeDueDate = newDate => {
|
||||
if (!modalChore) return
|
||||
UpdateDueDate(modalChore.id, newDate).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'rescheduled')
|
||||
})
|
||||
}
|
||||
})
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const handleCompleteWithPastDate = 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
|
||||
handleChoreUpdated(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const handleAssigneeChange = assigneeId => {
|
||||
if (!modalChore) return
|
||||
UpdateChoreAssignee(modalChore.id, assigneeId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = data.res
|
||||
handleChoreUpdated(newChore, 'assigned')
|
||||
})
|
||||
}
|
||||
})
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const handleCompleteWithNote = 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
|
||||
handleChoreUpdated(newChore, 'completed')
|
||||
})
|
||||
}
|
||||
})
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const handleNudge = 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()
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-update selected calendar date when day changes (large screen only)
|
||||
useEffect(() => {
|
||||
if (!isLargeScreen || viewMode !== 'calendar' || !selectedCalendarDate) {
|
||||
@@ -580,8 +693,9 @@ const MyChores = () => {
|
||||
|
||||
// Helper function to render the appropriate card component
|
||||
const renderChoreCard = (chore, key) => {
|
||||
performance.mark(`chore-render-start-${chore.id}`)
|
||||
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
|
||||
return (
|
||||
const result = (
|
||||
<CardComponent
|
||||
key={key || chore.id}
|
||||
chore={chore}
|
||||
@@ -590,16 +704,23 @@ const MyChores = () => {
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
onAction={handleChoreAction}
|
||||
// Multi-select props
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
isSelected={selectedChores.has(chore.id)}
|
||||
onSelectionToggle={() => toggleChoreSelection(chore.id)}
|
||||
/>
|
||||
)
|
||||
performance.mark(`chore-render-end-${chore.id}`)
|
||||
performance.measure(
|
||||
`chore-render-${chore.id}`,
|
||||
`chore-render-start-${chore.id}`,
|
||||
`chore-render-end-${chore.id}`,
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
const getFilteredChores = useMemo(() => {
|
||||
console.time('🏁 getFilteredChores')
|
||||
let result = []
|
||||
|
||||
if (searchTerm?.length > 0 || searchFilter !== 'All') {
|
||||
@@ -620,7 +741,6 @@ const MyChores = () => {
|
||||
)
|
||||
}
|
||||
|
||||
console.timeEnd('🏁 getFilteredChores')
|
||||
return result
|
||||
}, [
|
||||
searchTerm,
|
||||
@@ -634,7 +754,6 @@ const MyChores = () => {
|
||||
|
||||
const getChoresForDate = useCallback(
|
||||
date => {
|
||||
console.time('🏁 getChoresForDate')
|
||||
const filteredChoresData = getFilteredChores
|
||||
const result = filteredChoresData.filter(chore => {
|
||||
if (!chore.nextDueDate) return false
|
||||
@@ -642,30 +761,35 @@ const MyChores = () => {
|
||||
const selectedDate = date.toLocaleDateString()
|
||||
return choreDate === selectedDate
|
||||
})
|
||||
console.timeEnd('🏁 getChoresForDate')
|
||||
|
||||
return result
|
||||
},
|
||||
[getFilteredChores],
|
||||
)
|
||||
|
||||
const updateChores = useCallback(
|
||||
newChore => {
|
||||
console.time('🏁 updateChores')
|
||||
let newChores = [...chores, newChore]
|
||||
const updateChores = newChore => {
|
||||
let newChores = [...chores, newChore]
|
||||
|
||||
if (impersonatedUser) {
|
||||
newChores = newChores.filter(
|
||||
chore => chore.assignedTo === impersonatedUser.userId,
|
||||
)
|
||||
}
|
||||
// Filter chores based on impersonated user
|
||||
if (impersonatedUser) {
|
||||
newChores = newChores.filter(
|
||||
chore => chore.assignedTo === impersonatedUser.userId,
|
||||
)
|
||||
}
|
||||
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
setSearchFilter('All')
|
||||
console.timeEnd('🏁 updateChores')
|
||||
},
|
||||
[chores, impersonatedUser],
|
||||
)
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
setChoreSections(
|
||||
ChoresGrouper(
|
||||
selectedChoreSection,
|
||||
newChores,
|
||||
ChoreFilters(impersonatedUser?.userId || userProfile?.id)[
|
||||
selectedChoreFilter
|
||||
],
|
||||
),
|
||||
)
|
||||
setSearchFilter('All')
|
||||
}
|
||||
const handleMenuOutsideClick = event => {
|
||||
if (
|
||||
anchorEl &&
|
||||
@@ -706,109 +830,118 @@ const MyChores = () => {
|
||||
setSelectedCalendarDate(null)
|
||||
}
|
||||
|
||||
const handleChoreUpdated = useCallback(
|
||||
(updatedChore, event) => {
|
||||
console.time('🏁 handleChoreUpdated')
|
||||
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')
|
||||
) {
|
||||
newChores = newChores.filter(chore => chore.id !== updatedChore.id)
|
||||
newFilteredChores = newFilteredChores.filter(
|
||||
chore => chore.id !== updatedChore.id,
|
||||
)
|
||||
const handleChoreUpdated = (updatedChore, event) => {
|
||||
var newChores = chores.map(chore => {
|
||||
if (chore.id === updatedChore.id) {
|
||||
return updatedChore
|
||||
}
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
console.timeEnd('🏁 handleChoreUpdated')
|
||||
return chore
|
||||
})
|
||||
|
||||
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.',
|
||||
})
|
||||
var newFilteredChores = filteredChores.map(chore => {
|
||||
if (chore.id === updatedChore.id) {
|
||||
return updatedChore
|
||||
}
|
||||
},
|
||||
[chores, filteredChores, showSuccess, showWarning],
|
||||
)
|
||||
|
||||
const handleChoreDeleted = useCallback(
|
||||
deletedChore => {
|
||||
console.time('🏁 handleChoreDeleted')
|
||||
const newChores = chores.filter(chore => chore.id !== deletedChore.id)
|
||||
const newFilteredChores = filteredChores.filter(
|
||||
chore => chore.id !== deletedChore.id,
|
||||
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)
|
||||
console.timeEnd('🏁 handleChoreDeleted')
|
||||
},
|
||||
[chores, filteredChores],
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
() => ({
|
||||
@@ -1038,6 +1171,7 @@ const MyChores = () => {
|
||||
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be archived.`,
|
||||
})
|
||||
}
|
||||
refetchChores()
|
||||
clearSelection()
|
||||
} catch (error) {
|
||||
showError({
|
||||
@@ -1081,7 +1215,6 @@ const MyChores = () => {
|
||||
message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
|
||||
})
|
||||
|
||||
console.time('🏁 Bulk delete update')
|
||||
const deletedIds = new Set(deletedTasks.map(c => c.id))
|
||||
const newChores = chores.filter(c => !deletedIds.has(c.id))
|
||||
const newFilteredChores = filteredChores.filter(
|
||||
@@ -1089,7 +1222,6 @@ const MyChores = () => {
|
||||
)
|
||||
setChores(newChores)
|
||||
setFilteredChores(newFilteredChores)
|
||||
console.timeEnd('🏁 Bulk delete update')
|
||||
}
|
||||
|
||||
if (failedTasks.length > 0) {
|
||||
@@ -1098,7 +1230,7 @@ const MyChores = () => {
|
||||
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
|
||||
})
|
||||
}
|
||||
|
||||
refetchChores()
|
||||
clearSelection()
|
||||
} catch (error) {
|
||||
showError({
|
||||
@@ -1256,16 +1388,12 @@ const MyChores = () => {
|
||||
selectedItem={selectedChoreSection}
|
||||
selectedFilter={selectedChoreFilter}
|
||||
setFilter={filter => {
|
||||
console.time('🏁 Filter change')
|
||||
setSelectedChoreFilterWithCache(filter)
|
||||
console.timeEnd('🏁 Filter change')
|
||||
}}
|
||||
onItemSelect={selected => {
|
||||
console.time('🏁 Group by change')
|
||||
setSelectedChoreSectionWithCache(selected.value)
|
||||
setFilteredChores(chores)
|
||||
setSearchFilter('All')
|
||||
console.timeEnd('🏁 Group by change')
|
||||
}}
|
||||
mouseClickHandler={handleMenuOutsideClick}
|
||||
/>
|
||||
@@ -1825,7 +1953,7 @@ const MyChores = () => {
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{FILTERS['Overdue'](getFilteredChores()).length > 0 && (
|
||||
{FILTERS['Overdue'](getFilteredChores).length > 0 && (
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='danger'
|
||||
@@ -1837,8 +1965,7 @@ const MyChores = () => {
|
||||
})
|
||||
|
||||
// Also update state directly for immediate smooth transition
|
||||
const overdueChores =
|
||||
FILTERS['Overdue'](getFilteredChores())
|
||||
const overdueChores = FILTERS['Overdue'](getFilteredChores)
|
||||
setFilteredChores(overdueChores)
|
||||
setSearchFilter('Overdue')
|
||||
setViewMode('default')
|
||||
@@ -1852,7 +1979,7 @@ const MyChores = () => {
|
||||
}}
|
||||
startDecorator={
|
||||
<Chip size='md' variant='solid' color='danger'>
|
||||
{FILTERS['Overdue'](getFilteredChores()).length}
|
||||
{FILTERS['Overdue'](getFilteredChores).length}
|
||||
</Chip>
|
||||
}
|
||||
>
|
||||
@@ -1860,7 +1987,7 @@ const MyChores = () => {
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{FILTERS['No Due Date'](getFilteredChores()).length > 0 && (
|
||||
{FILTERS['No Due Date'](getFilteredChores).length > 0 && (
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
@@ -1873,7 +2000,7 @@ const MyChores = () => {
|
||||
|
||||
// Also update state directly for immediate smooth transition
|
||||
const unplannedChores =
|
||||
FILTERS['No Due Date'](getFilteredChores())
|
||||
FILTERS['No Due Date'](getFilteredChores)
|
||||
setFilteredChores(unplannedChores)
|
||||
setSearchFilter('No Due Date')
|
||||
setViewMode('default')
|
||||
@@ -1887,7 +2014,7 @@ const MyChores = () => {
|
||||
}}
|
||||
startDecorator={
|
||||
<Chip size='md' variant='solid' color='neutral'>
|
||||
{FILTERS['No Due Date'](getFilteredChores()).length}
|
||||
{FILTERS['No Due Date'](getFilteredChores).length}
|
||||
</Chip>
|
||||
}
|
||||
>
|
||||
@@ -1895,7 +2022,7 @@ const MyChores = () => {
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{FILTERS['Pending Approval'](getFilteredChores()).length > 0 && (
|
||||
{FILTERS['Pending Approval'](getFilteredChores).length > 0 && (
|
||||
<Chip
|
||||
variant='soft'
|
||||
size='lg'
|
||||
@@ -1907,7 +2034,7 @@ const MyChores = () => {
|
||||
|
||||
// Also update state directly for immediate smooth transition
|
||||
const pendingApprovalChores =
|
||||
FILTERS['Pending Approval'](getFilteredChores())
|
||||
FILTERS['Pending Approval'](getFilteredChores)
|
||||
setFilteredChores(pendingApprovalChores)
|
||||
setSearchFilter('Pending Approval')
|
||||
setViewMode('default')
|
||||
@@ -1928,7 +2055,7 @@ const MyChores = () => {
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{FILTERS['Pending Approval'](getFilteredChores()).length}
|
||||
{FILTERS['Pending Approval'](getFilteredChores).length}
|
||||
</Chip>
|
||||
}
|
||||
>
|
||||
@@ -1940,7 +2067,7 @@ const MyChores = () => {
|
||||
<Box sx={{ mb: 2 }}>
|
||||
{isLargeScreen ? (
|
||||
<CalendarDual
|
||||
chores={getFilteredChores()}
|
||||
chores={getFilteredChores}
|
||||
onDateChange={date => {
|
||||
setSelectedCalendarDate(date)
|
||||
}}
|
||||
@@ -1948,7 +2075,7 @@ const MyChores = () => {
|
||||
) : (
|
||||
<div className='calendar-dual'>
|
||||
<CalendarMonthly
|
||||
chores={getFilteredChores()}
|
||||
chores={getFilteredChores}
|
||||
onDateChange={date => {
|
||||
setSelectedCalendarDate(date)
|
||||
}}
|
||||
@@ -2170,12 +2297,78 @@ const MyChores = () => {
|
||||
<Sidepanel chores={chores} performers={performers} />
|
||||
|
||||
{/* Multi-select Help - only show when in multi-select mode */}
|
||||
<MultiSelectHelp isVisible={isMultiSelectMode} />
|
||||
{/* <MultiSelectHelp isVisible={isMultiSelectMode} /> */}
|
||||
|
||||
{/* Confirmation Modal for bulk operations */}
|
||||
{confirmModelConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
)}
|
||||
|
||||
{/* Centralized Modals */}
|
||||
{activeModal === 'changeDueDate' && modalChore && (
|
||||
<DateModal
|
||||
isOpen={true}
|
||||
key={'changeDueDate' + modalChore.id}
|
||||
current={modalChore.nextDueDate}
|
||||
title={`Change due date`}
|
||||
onClose={closeModal}
|
||||
onSave={handleChangeDueDate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModal === 'completeWithPastDate' && modalChore && (
|
||||
<DateModal
|
||||
isOpen={true}
|
||||
key={'completedInPast' + modalChore.id}
|
||||
current={modalChore.nextDueDate}
|
||||
title={`Save Chore that you completed in the past`}
|
||||
onClose={closeModal}
|
||||
onSave={handleCompleteWithPastDate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModal === 'changeAssignee' && modalChore && (
|
||||
<SelectModal
|
||||
isOpen={true}
|
||||
options={performers}
|
||||
displayKey='displayName'
|
||||
title={`Delegate to someone else`}
|
||||
placeholder={'Select a performer'}
|
||||
onClose={closeModal}
|
||||
onSave={selected => handleAssigneeChange(selected.id)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModal === 'completeWithNote' && modalChore && (
|
||||
<TextModal
|
||||
isOpen={true}
|
||||
title='Add note to attach to this completion:'
|
||||
onClose={closeModal}
|
||||
okText={'Complete'}
|
||||
onSave={handleCompleteWithNote}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModal === 'writeNFC' && modalChore && (
|
||||
<WriteNFCModal
|
||||
config={{
|
||||
isOpen: true,
|
||||
url: `${window.location.origin}/chores/${modalChore.id}`,
|
||||
onClose: closeModal,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModal === 'nudge' && modalChore && (
|
||||
<NudgeModal
|
||||
config={{
|
||||
isOpen: true,
|
||||
choreId: modalChore.id,
|
||||
onClose: closeModal,
|
||||
onConfirm: handleNudge,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user