enhance chore management with status updates and default assignee settings

This commit is contained in:
Mo Tarbin
2025-09-28 02:24:08 -04:00
parent c7c4a011ef
commit f1384eef34
4 changed files with 105 additions and 39 deletions

View File

@@ -59,13 +59,15 @@ export const useSSE = () => {
if (eventData.type === 'heartbeat') {
lastHeartbeatRef.current = Date.now()
}
console.log('SSE Message received:', eventData)
console.debug('SSE Message received:', eventData)
// Handle different event types and update React Query cache accordingly
switch (eventData.type) {
case 'chore.created':
case 'chore.updated':
case 'chore.completed':
case 'chore.status':
case 'chore.skipped': {
if (eventData?.data?.user?.id !== userProfile?.id) {
showNotification({
@@ -83,19 +85,25 @@ export const useSSE = () => {
return { res: { ...oldData.res, ...updatedChore } }
})
// If chore update then also refetch chore details:
if (
eventData.type === 'chore.updated' ||
eventData.type === 'chore.status'
) {
queryClient.invalidateQueries(['choreDetails', updatedChore.id])
queryClient.refetchQueries({
queryKey: ['choreDetails', updatedChore.id],
})
}
// Update chores list cache - add debugging
queryClient.setQueryData(['chores'], oldData => {
queryClient.setQueryData(['chores', false], oldData => {
if (!oldData) return { res: [updatedChore] }
if (!oldData.res || !Array.isArray(oldData.res)) {
return { res: [updatedChore] }
}
// Check if the chore exists in the cache
const choreExists = oldData.res.some(
chore => chore.id === updatedChore.id,
)
// If it's a one-time chore that's completed, we might need to remove it
if (
eventData.type === 'chore.completed' &&
@@ -108,25 +116,15 @@ export const useSSE = () => {
}
}
// If chore update then also refetch chore details:
if (eventData.type === 'chore.updated') {
queryClient.invalidateQueries(['choreDetails', updatedChore.id])
queryClient.refetchQueries({
queryKey: ['choreDetails', updatedChore.id],
})
}
// Otherwise update the existing chore or add if it doesn't exist
return {
res: choreExists
? oldData.res.map(chore => {
if (chore.id === updatedChore.id) {
return { ...chore, ...updatedChore }
}
return chore
})
: [...oldData.res, updatedChore],
}
const newData = oldData.res.map(chore => {
if (chore.id === updatedChore.id) {
return { ...updatedChore }
}
return chore
})
return { res: newData }
})
break
@@ -134,7 +132,16 @@ export const useSSE = () => {
case 'chore.deleted':
// update chores list cache
queryClient.setQueryData(['chores'], oldData => {
queryClient.setQueryData(['chores', false], oldData => {
if (!oldData || !oldData.res) return oldData
return {
res: oldData.res.filter(
chore => chore.id !== eventData.data.choreId,
),
}
})
// same logic for archived chores view:
queryClient.setQueryData(['chores', true], oldData => {
if (!oldData || !oldData.res) return oldData
return {
res: oldData.res.filter(
@@ -147,9 +154,27 @@ export const useSSE = () => {
case 'subtask.updated':
case 'subtask.completed':
queryClient.refetchQueries({
queryKey: ['choreDetails', eventData.data.choreId],
})
queryClient.setQueryData(
['choreDetails', String(eventData.data.choreId)], // this should be string to match the query key type which is param in the url in choreView
oldData => {
if (!oldData) return oldData
console.log('Old choreDetails data:', oldData)
// Update the specific subtask within the chore details
const newChoreData = { ...oldData.res }
newChoreData.subTasks = newChoreData.subTasks.map(subtask => {
if (subtask.id === eventData.data.subtaskId) {
return {
...subtask,
completedAt: eventData.data.completedAt,
completedBy: eventData.data.user.id,
}
}
return subtask
})
return { res: newChoreData }
},
)
// Invalidate the specific chore that contains this subtask
// if (eventData.data.choreId) {
@@ -162,10 +187,7 @@ export const useSSE = () => {
// Also invalidate general chores list
// queryClient.invalidateQueries(['chores'])
break
case 'chore.status':
console.log('SSE chore.status event received:', eventData.data)
break
case 'heartbeat':
// Heartbeat events don't need cache invalidation
console.debug('SSE Heartbeat received at', new Date().toISOString())

View File

@@ -110,6 +110,7 @@ const ChoreEdit = () => {
const [privacySaved, setPrivacySaved] = useState(false)
const [showSaveNotificationDefault, setShowSaveNotificationDefault] =
useState(false)
const [showSaveAssigneeDefault, setShowSaveAssigneeDefault] = useState(false)
const { data: userLabelsRaw, isLoading: isUserLabelsLoading } = useLabels()
const updateChoreMutation = useUpdateChore()
@@ -300,6 +301,14 @@ const ChoreEdit = () => {
if (defaultNotificationSetting !== null) {
setIsNotificable(JSON.parse(defaultNotificationSetting))
}
const defaultAssigneeSetting = localStorage.getItem(
'defaultAssigneeSetting',
)
if (defaultAssigneeSetting !== null) {
const savedAssignees = JSON.parse(defaultAssigneeSetting)
setAssignees(savedAssignees)
}
}
}, [])
useEffect(() => {
@@ -665,6 +674,7 @@ const ChoreEdit = () => {
} else {
setAssignees([...assignees, { userId: item.userId }])
}
setShowSaveAssigneeDefault(true)
}}
overlay
disableIcon
@@ -678,6 +688,33 @@ const ChoreEdit = () => {
<FormControl error={Boolean(errors.assignee)}>
<FormHelperText error>{Boolean(errors.assignee)}</FormHelperText>
</FormControl>
{showSaveAssigneeDefault && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'start' }}>
<Button
variant='outlined'
size='sm'
color='neutral'
startDecorator={<Save />}
sx={{
borderRadius: 6,
fontWeight: 500,
'&:hover': {
background: 'neutral.softHoverBg',
},
}}
onClick={() => {
localStorage.setItem(
'defaultAssigneeSetting',
JSON.stringify(assignees),
)
setShowSaveAssigneeDefault(false)
}}
>
Save Assignee Preference
</Button>
</Box>
)}
</Box>
{assignees.length > 1 && (

View File

@@ -44,6 +44,13 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
import {
useChoreTimer,
useDeleteTimeSession,
usePauseChore,
useResetChoreTimer,
useStartChore,
} from '../../queries/TimeQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
@@ -55,13 +62,6 @@ import {
SkipChore,
UpdateChorePriority,
} from '../../utils/Fetcher'
import {
useChoreTimer,
useDeleteTimeSession,
usePauseChore,
useResetChoreTimer,
useStartChore,
} from '../../queries/TimeQueries'
import Priorities from '../../utils/Priorities'
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'

View File

@@ -9,6 +9,7 @@ import {
} from '@mui/joy'
import Modal from '@mui/joy/Modal'
import ModalDialog from '@mui/joy/ModalDialog'
import { useQueryClient } from '@tanstack/react-query'
import imageCompression from 'browser-image-compression'
import { useRef, useState } from 'react'
import Cropper from 'react-easy-crop'
@@ -21,6 +22,7 @@ import { UploadFile } from '../../utils/TokenManager'
import SettingsLayout from './SettingsLayout'
const ProfileSettings = () => {
const queryClient = useQueryClient()
const { data: userProfile } = useUserProfile()
const { showSuccess, showError } = useNotification()
const [displayName, setDisplayName] = useState(userProfile?.displayName || '')
@@ -113,6 +115,9 @@ const ProfileSettings = () => {
try {
const userDetails = { displayName, timezone }
const response = await UpdateUserDetails(userDetails)
// invalidate user profile cache here if using react-query or similar:
queryClient.invalidateQueries(['userProfile'])
queryClient.refetchQueries(['userProfile'])
if (response.ok) {
showSuccess({
@@ -123,6 +128,8 @@ const ProfileSettings = () => {
throw new Error('Failed to update profile')
}
} catch (err) {
console.log(err)
showError({
title: 'Update Failed',
message: