Refactor chore management to use React Query hooks for timer and chore actions
- Replaced direct API calls with React Query hooks in ChoreView, ChoreCard, CompactChoreCard, and TimerDetails components for better state management and error handling. - Updated ArchivedTasks to utilize useUnArchiveChore hook for restoring archived chores. - Enhanced NotificationSetting to include device registration logic and improved user feedback for push notifications. - Refactored TimerEditModal and TimerDetails to streamline timer session updates and deletions using hooks. - Improved ChoreActionMenu to handle archiving and unarchiving chores with hooks. - Adjusted various components to use getSafeBottomStyles for consistent bottom padding. - Cleaned up unused imports and optimized loading states across components.
This commit is contained in:
10
src/App.jsx
10
src/App.jsx
@@ -10,6 +10,8 @@ import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
import { useNotification } from './service/NotificationProvider'
|
||||
import { apiManager } from './utils/TokenManager'
|
||||
|
||||
import { getSafeBottomPadding } from './utils/SafeAreaUtils'
|
||||
import NetworkBanner from './views/components/NetworkBanner'
|
||||
|
||||
const add = className => {
|
||||
@@ -87,21 +89,21 @@ const AppContent = () => {
|
||||
}, [needRefresh, showNotification, updateServiceWorker, setNeedRefresh])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingBottom: getSafeBottomPadding(2) }}>
|
||||
<ImpersonateUserProvider>
|
||||
<NavBar />
|
||||
<PageTransition>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
</ImpersonateUserProvider>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate()
|
||||
startApiManager(navigate)
|
||||
startOpenReplay()
|
||||
// startOpenReplay()
|
||||
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
|
||||
@@ -131,7 +133,7 @@ function App() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<div style={{ paddingBottom: getSafeBottomPadding(2) }}>
|
||||
<NetworkBanner />
|
||||
|
||||
<AuthenticationProvider>
|
||||
|
||||
@@ -23,8 +23,8 @@ const localNotificationListenerRegistration = () => {
|
||||
|
||||
const registerTokenIfNeeded = async (token, deviceInfo, deviceId, platform) => {
|
||||
try {
|
||||
const stored = await Preferences.get({ key: 'deviceRegistration' })
|
||||
const lastReg = stored.value ? JSON.parse(stored.value) : null
|
||||
// const stored = await Preferences.get({ key: 'deviceRegistration' })
|
||||
// const lastReg = stored.value ? JSON.parse(stored.value) : null
|
||||
|
||||
const current = {
|
||||
token: token.value,
|
||||
@@ -34,53 +34,86 @@ const registerTokenIfNeeded = async (token, deviceInfo, deviceId, platform) => {
|
||||
registeredAt: Date.now(),
|
||||
}
|
||||
|
||||
const shouldRegister =
|
||||
!lastReg ||
|
||||
lastReg.token !== current.token ||
|
||||
lastReg.appVersion !== current.appVersion ||
|
||||
Date.now() - lastReg.registeredAt > 7 * 24 * 60 * 60 * 1000
|
||||
// const shouldRegister =
|
||||
// !lastReg ||
|
||||
// lastReg.token !== current.token ||
|
||||
// lastReg.appVersion !== current.appVersion ||
|
||||
// Date.now() - lastReg.registeredAt > 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
if (shouldRegister) {
|
||||
console.log('Registering device token:', {
|
||||
reason: !lastReg
|
||||
? 'first_time'
|
||||
: lastReg.token !== current.token
|
||||
? 'token_changed'
|
||||
: lastReg.appVersion !== current.appVersion
|
||||
? 'app_updated'
|
||||
: 'periodic_refresh',
|
||||
})
|
||||
// console.log('Registering device token:', {
|
||||
// reason: !lastReg
|
||||
// ? 'first_time'
|
||||
// : lastReg.token !== current.token
|
||||
// ? 'token_changed'
|
||||
// : lastReg.appVersion !== current.appVersion
|
||||
// ? 'app_updated'
|
||||
// : 'periodic_refresh',
|
||||
// })
|
||||
|
||||
const result = await RegisterDeviceToken(
|
||||
token.value,
|
||||
deviceId.identifier,
|
||||
platform,
|
||||
deviceInfo.appVersion,
|
||||
deviceInfo.model,
|
||||
)
|
||||
|
||||
if (result && !result.error) {
|
||||
await Preferences.set({
|
||||
key: 'deviceRegistration',
|
||||
value: JSON.stringify(current),
|
||||
})
|
||||
console.log('Device token registered successfully')
|
||||
}
|
||||
} else {
|
||||
console.log('Device token already registered, skipping')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error in token registration check, registering anyway:',
|
||||
error,
|
||||
)
|
||||
await RegisterDeviceToken(
|
||||
const result = await RegisterDeviceToken(
|
||||
token.value,
|
||||
deviceId.identifier,
|
||||
platform,
|
||||
deviceInfo.appVersion,
|
||||
deviceInfo.model,
|
||||
)
|
||||
|
||||
if (result && result.ok) {
|
||||
await Preferences.set({
|
||||
key: 'deviceRegistration',
|
||||
value: JSON.stringify(current),
|
||||
})
|
||||
console.log('Device token registered successfully')
|
||||
|
||||
// Emit event to notify UI components of successful registration
|
||||
window.dispatchEvent(new CustomEvent('deviceTokenRegistered'))
|
||||
} else if (result) {
|
||||
// Handle registration errors
|
||||
console.error('Device registration failed:', result.status)
|
||||
|
||||
// Emit event with error details for UI to handle
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('deviceTokenRegistrationFailed', {
|
||||
detail: {
|
||||
status: result.status,
|
||||
error: await result.text().catch(() => 'Unknown error'),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error in token registration check, registering anyway:',
|
||||
error,
|
||||
)
|
||||
const fallbackResult = await RegisterDeviceToken(
|
||||
token.value,
|
||||
deviceId.identifier,
|
||||
platform,
|
||||
deviceInfo.appVersion,
|
||||
deviceInfo.model,
|
||||
)
|
||||
|
||||
if (fallbackResult && fallbackResult.ok) {
|
||||
// Emit event to notify UI components of successful registration
|
||||
window.dispatchEvent(new CustomEvent('deviceTokenRegistered'))
|
||||
} else if (fallbackResult) {
|
||||
// Handle registration errors
|
||||
console.error(
|
||||
'Fallback device registration failed:',
|
||||
fallbackResult.status,
|
||||
)
|
||||
|
||||
// Emit event with error details for UI to handle
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('deviceTokenRegistrationFailed', {
|
||||
detail: {
|
||||
status: fallbackResult.status,
|
||||
error: await fallbackResult.text().catch(() => 'Unknown error'),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import SSEProvider from './SSEContext'
|
||||
import ThemeContext from './ThemeContext'
|
||||
import WebSocketProvider from './WebSocketContext'
|
||||
|
||||
const Contexts = () => {
|
||||
const Contexts = ({ children }) => {
|
||||
const contexts = [
|
||||
AlertsProvider,
|
||||
ThemeContext,
|
||||
@@ -19,7 +19,7 @@ const Contexts = () => {
|
||||
|
||||
return contexts.reduceRight((acc, Context) => {
|
||||
return <Context>{acc}</Context>
|
||||
}, {})
|
||||
}, children)
|
||||
}
|
||||
|
||||
export default Contexts
|
||||
|
||||
@@ -4,10 +4,10 @@ const QueryContext = ({ children }) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60000, // 60 seconds
|
||||
gcTime: 300000, // 5 minutes
|
||||
staleTime: 300000, // 5 minutes
|
||||
gcTime: 600000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 0,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,12 +3,21 @@ import { useState } from 'react'
|
||||
import { networkManager } from '../hooks/NetworkManager'
|
||||
import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
|
||||
import {
|
||||
ApproveChore,
|
||||
ArchiveChore,
|
||||
CreateChore,
|
||||
DeleteChoreHistory,
|
||||
GetChoreByID,
|
||||
GetChoreDetailById,
|
||||
GetChoreHistory,
|
||||
GetChoresHistory,
|
||||
GetChoresNew,
|
||||
MarkChoreComplete,
|
||||
RejectChore,
|
||||
SaveChore,
|
||||
SkipChore,
|
||||
UnArchiveChore,
|
||||
UpdateChoreHistory,
|
||||
} from '../utils/Fetcher'
|
||||
import { localStore } from '../utils/LocalStore'
|
||||
|
||||
@@ -154,6 +163,8 @@ export const useUpdateChore = () => {
|
||||
onSuccess: (data, variables) => {
|
||||
// Invalidate the chores query to refresh the data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
// Invalidate history for the specific chore
|
||||
queryClient.invalidateQueries(['choreHistory', variables.id])
|
||||
},
|
||||
onMutate: async updatedChore => {
|
||||
if (!networkManager.isOnline && isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
@@ -255,3 +266,123 @@ export const useChore = choreId => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useArchiveChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ArchiveChore,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUnArchiveChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: UnArchiveChore,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useChoreHistory = choreId => {
|
||||
return useQuery({
|
||||
queryKey: ['choreHistory', choreId],
|
||||
queryFn: async () => {
|
||||
if (!choreId) {
|
||||
throw new Error('Chore ID is required to fetch history')
|
||||
}
|
||||
const response = await GetChoreHistory(choreId)
|
||||
if (response && response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
throw new Error('Failed to fetch chore history')
|
||||
},
|
||||
enabled: !!choreId,
|
||||
staleTime: 0, // Always consider data stale
|
||||
cacheTime: 0, // Don't cache the data
|
||||
refetchOnMount: true, // Always refetch when component mounts
|
||||
refetchOnWindowFocus: true, // Refetch when window gains focus
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateChoreHistory = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ choreId, historyId, historyData }) =>
|
||||
UpdateChoreHistory(choreId, historyId, historyData),
|
||||
onSuccess: (data, { choreId }) => {
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteChoreHistory = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ choreId, historyId }) =>
|
||||
DeleteChoreHistory(choreId, historyId),
|
||||
onSuccess: (data, { choreId }) => {
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMarkChoreComplete = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ choreId, body, completedDate, performer }) =>
|
||||
MarkChoreComplete(choreId, body, completedDate, performer),
|
||||
onSuccess: (data, { choreId }) => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
queryClient.invalidateQueries(['choreDetails', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useSkipChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: SkipChore,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
queryClient.invalidateQueries(['choreDetails', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useApproveChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ApproveChore,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
queryClient.invalidateQueries(['choreDetails', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRejectChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: RejectChore,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
queryClient.invalidateQueries(['choreDetails', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GetResource } from '../utils/Fetcher'
|
||||
|
||||
export const useResource = () => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: [],
|
||||
queryKey: ['resource'],
|
||||
queryFn: async () => {
|
||||
const response = await GetResource()
|
||||
return response
|
||||
|
||||
107
src/queries/TimeQueries.jsx
Normal file
107
src/queries/TimeQueries.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
ClearChoreTimer,
|
||||
DeleteTimeSession,
|
||||
GetChoreTimer,
|
||||
PauseChore,
|
||||
ResetChoreTimer,
|
||||
StartChore,
|
||||
UpdateTimeSession,
|
||||
} from '../utils/Fetcher'
|
||||
|
||||
export const useChoreTimer = choreId => {
|
||||
return useQuery({
|
||||
queryKey: ['choreTimer', choreId],
|
||||
queryFn: async () => {
|
||||
if (!choreId) {
|
||||
throw new Error('Chore ID is required to fetch timer')
|
||||
}
|
||||
const response = await GetChoreTimer(choreId)
|
||||
if (response && response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
throw new Error('Failed to fetch chore timer')
|
||||
},
|
||||
enabled: !!choreId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useStartChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: StartChore,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const usePauseChore = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: PauseChore,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateTimeSession = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ choreId, sessionId, sessionData }) =>
|
||||
UpdateTimeSession(choreId, sessionId, sessionData),
|
||||
onSuccess: (data, { choreId }) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteTimeSession = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ choreId, sessionId }) =>
|
||||
DeleteTimeSession(choreId, sessionId),
|
||||
onSuccess: (data, { choreId }) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useResetChoreTimer = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ResetChoreTimer,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useClearChoreTimer = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ClearChoreTimer,
|
||||
onSuccess: (data, choreId) => {
|
||||
queryClient.invalidateQueries(['choreTimer', choreId])
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
queryClient.invalidateQueries(['choreHistory', choreId])
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const useDeviceTokens = () => {
|
||||
const result = await resp.json()
|
||||
return result.res || []
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 0, // Always fetch fresh data
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
})
|
||||
|
||||
|
||||
@@ -99,17 +99,20 @@ export const isOfficialDonetickInstance = async () => {
|
||||
*/
|
||||
export const isOfficialDonetickInstanceSync = () => {
|
||||
try {
|
||||
// Import here to avoid circular dependencies
|
||||
const { apiManager } = require('../utils/TokenManager')
|
||||
|
||||
const currentApiUrl = apiManager.getApiURL()
|
||||
|
||||
// Check if the API URL contains donetick.com
|
||||
return currentApiUrl.toLowerCase().includes('donetick.com')
|
||||
// Dynamic import to avoid circular dependencies
|
||||
return import('../utils/TokenManager').then(({ apiManager }) => {
|
||||
const currentApiUrl = apiManager.getApiURL()
|
||||
// Check if the API URL contains donetick.com
|
||||
return currentApiUrl.toLowerCase().includes('donetick.com')
|
||||
}).catch(error => {
|
||||
console.warn('FeatureToggle: Error checking server instance (sync):', error)
|
||||
// Default to false for safety (self-hosted assumption)
|
||||
return false
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('FeatureToggle: Error checking server instance (sync):', error)
|
||||
// Default to false for safety (self-hosted assumption)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Preferences } from '@capacitor/preferences'
|
||||
import Cookies from 'js-cookie'
|
||||
import murmurhash from 'murmurhash'
|
||||
import { API_URL } from '../Config'
|
||||
import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
|
||||
import { networkManager } from '../hooks/NetworkManager'
|
||||
import { RefreshToken } from './Fetcher'
|
||||
import { localStore } from './LocalStore'
|
||||
@@ -105,11 +106,14 @@ export async function Fetch(url, options) {
|
||||
const response = await fetch(fullURL, options)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.clone().json()
|
||||
const optionWithoutToken = { ...options }
|
||||
delete optionWithoutToken.headers.Authorization
|
||||
const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
|
||||
await localStore.saveToCache(fullURL + optionsHash, data)
|
||||
// Only cache data if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
const data = await response.clone().json()
|
||||
const optionWithoutToken = { ...options }
|
||||
delete optionWithoutToken.headers.Authorization
|
||||
const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
|
||||
await localStore.saveToCache(fullURL + optionsHash, data)
|
||||
}
|
||||
networkManager.setOnline()
|
||||
} else if (response.status === 401) {
|
||||
// Handle 401 Unauthorized
|
||||
@@ -124,15 +128,24 @@ export async function Fetch(url, options) {
|
||||
response.status === 0
|
||||
) {
|
||||
networkManager.setOffline()
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
}
|
||||
// If offline mode is disabled, just throw the error
|
||||
throw new Error(`Request failed with status ${response.status}`)
|
||||
}
|
||||
// return promise that resolves to response object:
|
||||
return Promise.resolve(response)
|
||||
} catch (error) {
|
||||
networkManager.setOffline()
|
||||
console.error('Fetch error:', error)
|
||||
// throw error
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
}
|
||||
// If offline mode is disabled, just throw the error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +197,11 @@ export const refreshAccessToken = () => {
|
||||
}
|
||||
|
||||
async function handleOfflineRequest(url, options) {
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
throw new Error('Network request failed and offline mode is disabled')
|
||||
}
|
||||
|
||||
// if get request then attempt to fetch from cache otherewise queue it :
|
||||
if (options.method === 'GET') {
|
||||
return attemptFetchFromCache(url, options)
|
||||
@@ -200,6 +218,11 @@ async function handleOfflineRequest(url, options) {
|
||||
}
|
||||
}
|
||||
async function attemptFetchFromCache(url, options) {
|
||||
// Only attempt cache fetch if offline mode is enabled
|
||||
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
throw new Error('Cache access disabled - offline mode is not enabled')
|
||||
}
|
||||
|
||||
const optionsHash = murmurhash.v3(JSON.stringify(options))
|
||||
const cachedData = await localStore.getFromCache(url + optionsHash)
|
||||
networkManager.setOffline()
|
||||
|
||||
@@ -49,18 +49,21 @@ import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteTimeSession,
|
||||
GetChoreDetailById,
|
||||
GetChoreTimer,
|
||||
MarkChoreComplete,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
ResetChoreTimer,
|
||||
SkipChore,
|
||||
StartChore,
|
||||
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'
|
||||
import LoadingComponent from '../components/Loading.jsx'
|
||||
import RichTextEditor from '../components/RichTextEditor.jsx'
|
||||
@@ -95,6 +98,12 @@ const ChoreView = () => {
|
||||
const { data: choreData, isLoading: isChoreLoading } =
|
||||
useChoreDetails(choreId)
|
||||
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
const deleteTimeSession = useDeleteTimeSession()
|
||||
const resetChoreTimer = useResetChoreTimer()
|
||||
const { data: choreTimer } = useChoreTimer(choreId)
|
||||
|
||||
useEffect(() => {
|
||||
if (!choreData || !choreData.res || !circleMembersData) {
|
||||
return
|
||||
@@ -241,30 +250,26 @@ const ChoreView = () => {
|
||||
})
|
||||
}
|
||||
const handleChoreStart = () => {
|
||||
StartChore(choreId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
})
|
||||
}
|
||||
startChore.mutate(choreId, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleChorePause = () => {
|
||||
PauseChore(choreId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
})
|
||||
}
|
||||
pauseChore.mutate(choreId, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,17 +283,14 @@ const ChoreView = () => {
|
||||
cancelText: 'Cancel',
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
ResetChoreTimer(choreId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
})
|
||||
}
|
||||
resetChoreTimer.mutate(choreId, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
},
|
||||
})
|
||||
}
|
||||
setTimerActionConfig({})
|
||||
@@ -306,22 +308,19 @@ const ChoreView = () => {
|
||||
cancelText: 'Cancel',
|
||||
onClose: async confirmed => {
|
||||
if (confirmed) {
|
||||
const resp = await GetChoreTimer(choreId)
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
const sessionId = data?.res?.id
|
||||
DeleteTimeSession(choreId, sessionId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
if (choreTimer?.res?.id) {
|
||||
deleteTimeSession.mutate(
|
||||
{ choreId, sessionId: choreTimer.res.id },
|
||||
{
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
setChore(newChore)
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
setTimerActionConfig({})
|
||||
@@ -712,6 +711,7 @@ const ChoreView = () => {
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
boxShadow: 'sm',
|
||||
paddingBottom: getSafeBottomPadding(2, '8px'),
|
||||
}}
|
||||
variant='soft'
|
||||
>
|
||||
|
||||
@@ -25,14 +25,11 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useUnArchiveChore } from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ChoreSorter } from '../../utils/Chores'
|
||||
import {
|
||||
DeleteChore,
|
||||
GetArchivedChores,
|
||||
UnArchiveChore,
|
||||
} from '../../utils/Fetcher'
|
||||
import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import ChoreCard from './ChoreCard'
|
||||
@@ -44,6 +41,7 @@ const ArchivedTasks = () => {
|
||||
useUserProfile()
|
||||
const { showSuccess, showError } = useNotification()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
const unArchiveChore = useUnArchiveChore()
|
||||
const [archivedChores, setArchivedChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -317,10 +315,20 @@ const ArchivedTasks = () => {
|
||||
|
||||
for (const chore of selectedData) {
|
||||
try {
|
||||
await UnArchiveChore(chore.id)
|
||||
restoredTasks.push(chore)
|
||||
await new Promise((resolve, reject) => {
|
||||
unArchiveChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
restoredTasks.push(chore)
|
||||
resolve(data)
|
||||
},
|
||||
onError: error => {
|
||||
failedTasks.push(chore)
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
failedTasks.push(chore)
|
||||
// Error already handled in onError callback
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ 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'
|
||||
@@ -42,9 +43,7 @@ import {
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
StartChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
@@ -91,6 +90,8 @@ const ChoreCard = ({
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { showError, showNotification } = useNotification()
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
|
||||
// Swipe functionality state
|
||||
const [swipeTranslateX, setSwipeTranslateX] = React.useState(0)
|
||||
@@ -109,7 +110,7 @@ const ChoreCard = ({
|
||||
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0)
|
||||
}
|
||||
checkTouchDevice()
|
||||
|
||||
|
||||
// Check if this is the official donetick.com instance
|
||||
try {
|
||||
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
||||
@@ -277,7 +278,10 @@ const ChoreCard = ({
|
||||
|
||||
const handleNudge = async ({ choreId, message, notifyAllAssignees }) => {
|
||||
try {
|
||||
const response = await NudgeChore(choreId, { message, notifyAllAssignees })
|
||||
const response = await NudgeChore(choreId, {
|
||||
message,
|
||||
notifyAllAssignees,
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
showNotification({
|
||||
@@ -490,30 +494,26 @@ const ChoreCard = ({
|
||||
|
||||
// Handlers for start/pause/complete functionality
|
||||
const handleChorePause = () => {
|
||||
PauseChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
})
|
||||
}
|
||||
pauseChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleChoreStart = () => {
|
||||
StartChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
})
|
||||
}
|
||||
startChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
status: data.res.status,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -836,7 +836,7 @@ const ChoreCard = ({
|
||||
<Edit sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
|
||||
{isOfficialInstance && (
|
||||
{isOfficialInstance && (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='warning'
|
||||
|
||||
@@ -41,12 +41,11 @@ import {
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
StartChore,
|
||||
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'
|
||||
@@ -81,6 +80,8 @@ const CompactChoreCard = ({
|
||||
const [isNudgeModalOpen, setIsNudgeModalOpen] = React.useState(false)
|
||||
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)
|
||||
@@ -656,29 +657,25 @@ const CompactChoreCard = ({
|
||||
}
|
||||
|
||||
const handleChorePause = () => {
|
||||
PauseChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
})
|
||||
}
|
||||
pauseChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'paused')
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleChoreStart = () => {
|
||||
StartChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
})
|
||||
}
|
||||
startChore.mutate(chore.id, {
|
||||
onSuccess: data => {
|
||||
const newChore = {
|
||||
...chore,
|
||||
...data.res,
|
||||
}
|
||||
onChoreUpdate(newChore, 'started')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,9 @@ import {
|
||||
import Fuse from 'fuse.js'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { useChores } from '../../queries/ChoreQueries'
|
||||
import { useChores, useArchiveChore } from '../../queries/ChoreQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { TASK_COLOR } from '../../utils/Colors'
|
||||
import { ArchiveChore } from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import { useLabels } from '../Labels/LabelQueries'
|
||||
@@ -76,6 +75,7 @@ const MyChores = () => {
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||
const { showSuccess, showError, showWarning } = useNotification()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
const archiveChore = useArchiveChore()
|
||||
const [chores, setChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [searchFilter, setSearchFilter] = useState('All')
|
||||
@@ -920,13 +920,23 @@ const MyChores = () => {
|
||||
const failedTasks = []
|
||||
for (const chore of selectedData) {
|
||||
try {
|
||||
const archivedChore = await ArchiveChore(chore.id)
|
||||
archivedTasks.push(archivedChore)
|
||||
// Remove from chores and filteredChores
|
||||
setChores(chores.filter(c => c.id !== chore.id))
|
||||
setFilteredChores(filteredChores.filter(c => c.id !== chore.id))
|
||||
await new Promise((resolve, reject) => {
|
||||
archiveChore.mutate(chore.id, {
|
||||
onSuccess: (data) => {
|
||||
archivedTasks.push(data)
|
||||
// Remove from chores and filteredChores
|
||||
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) {
|
||||
failedTasks.push(chore)
|
||||
// Error already handled in onError callback
|
||||
}
|
||||
}
|
||||
if (archivedTasks.length > 0) {
|
||||
|
||||
@@ -24,39 +24,41 @@ import { Link, useParams } from 'react-router-dom'
|
||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||
import { ChoreHistoryStatus } from '../../utils/Chores'
|
||||
import {
|
||||
DeleteChoreHistory,
|
||||
GetAllCircleMembers,
|
||||
GetChoreHistory,
|
||||
UpdateChoreHistory,
|
||||
} from '../../utils/Fetcher'
|
||||
useChoreHistory,
|
||||
useDeleteChoreHistory,
|
||||
useUpdateChoreHistory,
|
||||
} from '../../queries/ChoreQueries'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import EditHistoryModal from '../Modals/EditHistoryModal'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import HistoryCard from './HistoryCard'
|
||||
|
||||
const ChoreHistory = () => {
|
||||
const [choreHistory, setChoresHistory] = useState([])
|
||||
const [userHistory, setUserHistory] = useState([])
|
||||
const [performers, setPerformers] = useState([])
|
||||
const [historyInfo, setHistoryInfo] = useState([])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true) // Add loading state
|
||||
const { choreId } = useParams()
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [editHistory, setEditHistory] = useState({})
|
||||
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
||||
|
||||
// React Query hooks
|
||||
const { data: choreHistoryData, isLoading } = useChoreHistory(choreId)
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
const updateChoreHistory = useUpdateChoreHistory()
|
||||
const deleteChoreHistory = useDeleteChoreHistory()
|
||||
|
||||
const choreHistory = choreHistoryData?.res || []
|
||||
const performers = circleMembersData?.res || []
|
||||
|
||||
const handleDelete = historyEntry => {
|
||||
showConfirmation(
|
||||
`Are you sure you want to delete this history record?`,
|
||||
'Delete History Record',
|
||||
() => {
|
||||
DeleteChoreHistory(choreId, historyEntry.id).then(() => {
|
||||
const newHistory = choreHistory.filter(
|
||||
record => record.id !== historyEntry.id,
|
||||
)
|
||||
setChoresHistory(newHistory)
|
||||
updateHistoryInfo(newHistory, userHistory, performers)
|
||||
deleteChoreHistory.mutate({
|
||||
choreId,
|
||||
historyId: historyEntry.id,
|
||||
})
|
||||
},
|
||||
'Delete',
|
||||
@@ -71,33 +73,16 @@ const ChoreHistory = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true) // Start loading
|
||||
|
||||
Promise.all([
|
||||
GetChoreHistory(choreId).then(res => res.json()),
|
||||
GetAllCircleMembers(),
|
||||
])
|
||||
.then(([historyData, usersData]) => {
|
||||
setChoresHistory(historyData.res)
|
||||
|
||||
const newUserChoreHistory = {}
|
||||
historyData.res.forEach(choreHistory => {
|
||||
const userId = choreHistory.completedBy
|
||||
newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1
|
||||
})
|
||||
setUserHistory(newUserChoreHistory)
|
||||
|
||||
setPerformers(usersData.res)
|
||||
updateHistoryInfo(historyData.res, newUserChoreHistory, usersData.res)
|
||||
if (choreHistory.length > 0 && performers.length > 0) {
|
||||
const newUserChoreHistory = {}
|
||||
choreHistory.forEach(historyEntry => {
|
||||
const userId = historyEntry.completedBy
|
||||
newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error)
|
||||
// Handle errors, e.g., show an error message to the user
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false) // Finish loading
|
||||
})
|
||||
}, [choreId])
|
||||
setUserHistory(newUserChoreHistory)
|
||||
updateHistoryInfo(choreHistory, newUserChoreHistory, performers)
|
||||
}
|
||||
}, [choreHistory, performers])
|
||||
|
||||
const updateHistoryInfo = (histories, userHistories, performers) => {
|
||||
// average delay for task completaion from due date:
|
||||
@@ -327,35 +312,39 @@ const ChoreHistory = () => {
|
||||
setIsEditModalOpen(false)
|
||||
},
|
||||
onSave: updated => {
|
||||
UpdateChoreHistory(choreId, editHistory.id, {
|
||||
performedAt: updated.performedAt,
|
||||
dueDate: updated.dueDate,
|
||||
notes: updated.notes,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
console.error('Failed to update chore history:', res)
|
||||
return
|
||||
}
|
||||
|
||||
const newRecord = res.json().then(data => {
|
||||
const newRecord = data.res
|
||||
const newHistory = choreHistory.map(record =>
|
||||
record.id === newRecord.id ? newRecord : record,
|
||||
)
|
||||
setChoresHistory(newHistory)
|
||||
setEditHistory(newRecord)
|
||||
setIsEditModalOpen(false)
|
||||
})
|
||||
})
|
||||
updateChoreHistory.mutate(
|
||||
{
|
||||
choreId,
|
||||
historyId: editHistory.id,
|
||||
historyData: {
|
||||
performedAt: updated.performedAt,
|
||||
dueDate: updated.dueDate,
|
||||
notes: updated.notes,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: data => {
|
||||
setEditHistory(data.res)
|
||||
setIsEditModalOpen(false)
|
||||
},
|
||||
onError: error => {
|
||||
console.error('Failed to update chore history:', error)
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
onDelete: () => {
|
||||
DeleteChoreHistory(choreId, editHistory.id).then(() => {
|
||||
const newHistory = choreHistory.filter(
|
||||
record => record.id !== editHistory.id,
|
||||
)
|
||||
setChoresHistory(newHistory)
|
||||
setIsEditModalOpen(false)
|
||||
})
|
||||
deleteChoreHistory.mutate(
|
||||
{
|
||||
choreId,
|
||||
historyId: editHistory.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsEditModalOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
}}
|
||||
historyRecord={editHistory}
|
||||
|
||||
@@ -502,30 +502,23 @@ const HistoryCard = ({
|
||||
>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
startDecorator={<Person />}
|
||||
variant='solid'
|
||||
color='success'
|
||||
startDecorator={<CheckCircle />}
|
||||
>
|
||||
{performer?.displayName || 'Unknown'}
|
||||
Done by {performer?.displayName || 'Unknown'}
|
||||
</Chip>
|
||||
|
||||
{historyEntry.completedBy !== historyEntry.assignedTo &&
|
||||
assignedTo && (
|
||||
<>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.tertiary' }}
|
||||
>
|
||||
→
|
||||
</Typography>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
startDecorator={<CheckCircle />}
|
||||
>
|
||||
{assignedTo.displayName}
|
||||
</Chip>
|
||||
</>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<Person />}
|
||||
>
|
||||
Assigned to {assignedTo.displayName}
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{historyEntry.notes && (
|
||||
|
||||
@@ -21,7 +21,7 @@ import LABEL_COLORS, {
|
||||
getTextColorFromBackgroundColor,
|
||||
} from '../../utils/Colors'
|
||||
import { DeleteLabel } from '../../utils/Fetcher'
|
||||
import { getSafeBottom, getSafeBottomStyles } from '../../utils/SafeAreaUtils'
|
||||
import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import { useLabels } from './LabelQueries'
|
||||
|
||||
@@ -222,7 +222,7 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: getSafeBottom(),
|
||||
bottom: 0,
|
||||
width: maxSwipeDistance,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -15,10 +15,10 @@ import { useEffect, useState } from 'react'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useNotification } from '../../../service/NotificationProvider'
|
||||
import {
|
||||
DeleteTimeSession,
|
||||
GetChoreTimer,
|
||||
UpdateTimeSession,
|
||||
} from '../../../utils/Fetcher'
|
||||
useChoreTimer,
|
||||
useDeleteTimeSession,
|
||||
useUpdateTimeSession,
|
||||
} from '../../../queries/TimeQueries'
|
||||
import ConfirmationModal from './ConfirmationModal'
|
||||
|
||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
@@ -31,13 +31,17 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
const { showError, showSuccess } = useNotification()
|
||||
|
||||
// Fetch timer data when modal opens
|
||||
// Timer hooks
|
||||
const { data: choreTimer, refetch: refetchTimer } = useChoreTimer(choreId)
|
||||
const updateTimeSession = useUpdateTimeSession()
|
||||
const deleteTimeSession = useDeleteTimeSession()
|
||||
|
||||
// Update timerData when choreTimer data changes
|
||||
useEffect(() => {
|
||||
if (isOpen && choreId) {
|
||||
fetchTimerData()
|
||||
if (choreTimer?.res) {
|
||||
setTimerData(choreTimer.res)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, choreId])
|
||||
}, [choreTimer])
|
||||
|
||||
// Real-time update interval for active timers
|
||||
useEffect(() => {
|
||||
@@ -53,28 +57,6 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
}
|
||||
}, [isOpen, timerData])
|
||||
|
||||
const fetchTimerData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await GetChoreTimer(choreId)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setTimerData(data.res) // data.res is the timer session object
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to fetch timer data',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error fetching timer data',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = seconds => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
@@ -192,21 +174,26 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
pauseLog: editingData.pauseLog,
|
||||
}
|
||||
|
||||
const response = await UpdateTimeSession(choreId, sessionId, updateData)
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
cancelEditingSession(sessionId)
|
||||
onTimerUpdate?.()
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
updateTimeSession.mutate(
|
||||
{ choreId, sessionId, sessionData: updateData },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
})
|
||||
refetchTimer()
|
||||
cancelEditingSession(sessionId)
|
||||
onTimerUpdate?.()
|
||||
},
|
||||
onError: () => {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error updating session',
|
||||
@@ -219,29 +206,28 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
|
||||
const deleteSession = async sessionId => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await DeleteTimeSession(choreId, sessionId)
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Session deleted',
|
||||
message: 'Timer session has been deleted successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
onTimerUpdate?.()
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to delete session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error deleting session',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
deleteTimeSession.mutate(
|
||||
{ choreId, sessionId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Session deleted',
|
||||
message: 'Timer session has been deleted successfully.',
|
||||
})
|
||||
refetchTimer()
|
||||
onTimerUpdate?.()
|
||||
},
|
||||
onError: error => {
|
||||
showError({
|
||||
title: 'Error deleting session',
|
||||
message: error.message,
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
setLoading(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const confirmDeleteSession = sessionId => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Device } from '@capacitor/device'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { Android, Apple } from '@mui/icons-material'
|
||||
@@ -72,6 +73,9 @@ const NotificationSetting = () => {
|
||||
const [naggingNotification, setNaggingNotification] = useState(false)
|
||||
const [pushNotification, setPushNotification] = useState(false)
|
||||
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
||||
const [currentDevice, setCurrentDevice] = useState(null)
|
||||
const [isCurrentDeviceRegistered, setIsCurrentDeviceRegistered] =
|
||||
useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
getNotificationPreferences().then(resp => {
|
||||
@@ -95,6 +99,28 @@ const NotificationSetting = () => {
|
||||
console.warn('Error checking instance type:', error)
|
||||
setIsOfficialInstance(false)
|
||||
}
|
||||
|
||||
// Get current device info if on native platform
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
const getCurrentDeviceInfo = async () => {
|
||||
try {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
const deviceId = await Device.getId()
|
||||
const platform =
|
||||
Capacitor.getPlatform() === 'android' ? 'android' : 'ios'
|
||||
|
||||
setCurrentDevice({
|
||||
id: deviceId.identifier,
|
||||
platform,
|
||||
model: deviceInfo.model,
|
||||
appVersion: deviceInfo.appVersion,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error getting device info:', error)
|
||||
}
|
||||
}
|
||||
getCurrentDeviceInfo()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [notificationTarget, setNotificationTarget] = useState(
|
||||
@@ -107,6 +133,64 @@ const NotificationSetting = () => {
|
||||
userProfile?.notification_target?.target_id ?? 0,
|
||||
)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// Check if current device is registered whenever deviceTokens or currentDevice changes
|
||||
useEffect(() => {
|
||||
if (currentDevice && deviceTokens && isOfficialInstance) {
|
||||
const isRegistered = deviceTokens.some(
|
||||
device => device.deviceId === currentDevice.id,
|
||||
)
|
||||
setIsCurrentDeviceRegistered(isRegistered)
|
||||
}
|
||||
}, [currentDevice, deviceTokens, isOfficialInstance])
|
||||
|
||||
// Listen for device registration events from CapacitorListener
|
||||
useEffect(() => {
|
||||
const handleDeviceRegistered = () => {
|
||||
refetchDevices()
|
||||
showWarning({
|
||||
title: 'Success',
|
||||
message: 'Device registered successfully for push notifications.',
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeviceRegistrationFailed = event => {
|
||||
const { status, error } = event.detail || {}
|
||||
|
||||
if (status === 409) {
|
||||
showWarning({
|
||||
title: 'Device Limit Reached',
|
||||
message:
|
||||
'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
|
||||
})
|
||||
} else {
|
||||
showWarning({
|
||||
title: 'Registration Failed',
|
||||
message:
|
||||
error ||
|
||||
'Failed to register device automatically. Please try again.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for the custom events that CapacitorListener might emit
|
||||
window.addEventListener('deviceTokenRegistered', handleDeviceRegistered)
|
||||
window.addEventListener(
|
||||
'deviceTokenRegistrationFailed',
|
||||
handleDeviceRegistrationFailed,
|
||||
)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'deviceTokenRegistered',
|
||||
handleDeviceRegistered,
|
||||
)
|
||||
window.removeEventListener(
|
||||
'deviceTokenRegistrationFailed',
|
||||
handleDeviceRegistrationFailed,
|
||||
)
|
||||
}
|
||||
}, [refetchDevices, showWarning])
|
||||
const SaveValidation = () => {
|
||||
switch (notificationTarget) {
|
||||
case '1':
|
||||
@@ -146,6 +230,55 @@ const NotificationSetting = () => {
|
||||
alert('Notification target updated')
|
||||
})
|
||||
}
|
||||
|
||||
const handleRegisterCurrentDevice = async () => {
|
||||
if (!currentDevice) return
|
||||
|
||||
// Check device limit before attempting registration
|
||||
const currentDeviceCount = deviceTokens ? deviceTokens.length : 0
|
||||
if (currentDeviceCount >= 5) {
|
||||
showWarning({
|
||||
title: 'Device Limit Reached',
|
||||
message:
|
||||
'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// First request push notification permission
|
||||
const permStatus = await PushNotifications.requestPermissions()
|
||||
|
||||
if (permStatus.receive !== 'granted') {
|
||||
showWarning({
|
||||
title: 'Permission Required',
|
||||
message:
|
||||
'Push notification permission is required to register this device.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure push notification listeners are set up before registration
|
||||
|
||||
await registerPushNotifications()
|
||||
|
||||
// Store registration preferences immediately since permission was granted
|
||||
await setPushNotificationPreferences({ granted: true })
|
||||
setPushNotification(true)
|
||||
|
||||
showWarning({
|
||||
title: 'Registration Initiated',
|
||||
message:
|
||||
'Push notification registration has been initiated. The device will be registered automatically.',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error registering device:', error)
|
||||
showWarning({
|
||||
title: 'Error',
|
||||
message: 'Failed to register device. Please try again.',
|
||||
})
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SettingsLayout title='Notification Settings'>
|
||||
<div className='grid gap-4 py-4' id='notifications'>
|
||||
@@ -344,13 +477,60 @@ const NotificationSetting = () => {
|
||||
{isOfficialInstance && (
|
||||
<>
|
||||
<Typography level='h4' sx={{ mt: 2 }}>
|
||||
Registered Devices
|
||||
Registered Devices ({deviceTokens ? deviceTokens.length : 0}/5)
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md' sx={{ mb: 2 }}>
|
||||
Devices registered to receive push notifications for your account
|
||||
</Typography>
|
||||
|
||||
{/* Show register current device option if not registered */}
|
||||
{Capacitor.isNativePlatform() &&
|
||||
currentDevice &&
|
||||
!isCurrentDeviceRegistered && (
|
||||
<Card
|
||||
variant='outlined'
|
||||
sx={{ p: 2, mb: 2, bgcolor: 'background.level1' }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{currentDevice.platform === 'ios' ? (
|
||||
<Apple sx={{ fontSize: 24, color: '#007AFF' }} />
|
||||
) : (
|
||||
<Android sx={{ fontSize: 24, color: '#3DDC84' }} />
|
||||
)}
|
||||
<Box>
|
||||
<Typography level='body-md' sx={{ fontWeight: 'bold' }}>
|
||||
Current Device:{' '}
|
||||
{currentDevice.platform === 'ios' ? 'iOS' : 'Android'}{' '}
|
||||
{currentDevice.model}
|
||||
</Typography>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
This device is not registered for push notifications
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
size='sm'
|
||||
disabled={deviceTokens && deviceTokens.length >= 5}
|
||||
onClick={handleRegisterCurrentDevice}
|
||||
>
|
||||
{deviceTokens && deviceTokens.length >= 5
|
||||
? 'Limit Reached'
|
||||
: 'Register Device'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{deviceTokens && deviceTokens.length > 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{deviceTokens.map(device => (
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
SaveThing,
|
||||
UpdateThingState,
|
||||
} from '../../utils/Fetcher'
|
||||
import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import CreateThingModal from '../Modals/Inputs/CreateThingModal'
|
||||
import EditThingStateModal from '../Modals/Inputs/EditThingState'
|
||||
@@ -724,14 +725,11 @@ const ThingsView = () => {
|
||||
<Box
|
||||
// variant='outlined'
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
...getSafeBottomStyles({ bottom: 0, padding: 16 }),
|
||||
left: 10,
|
||||
p: 2, // padding
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 2,
|
||||
|
||||
'z-index': 1000,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -30,11 +30,11 @@ import { useParams } from 'react-router-dom'
|
||||
import { useCircleMembers } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import {
|
||||
GetChoreTimer,
|
||||
PauseChore,
|
||||
StartChore,
|
||||
UpdateTimeSession,
|
||||
} from '../../utils/Fetcher'
|
||||
useChoreTimer,
|
||||
usePauseChore,
|
||||
useStartChore,
|
||||
useUpdateTimeSession,
|
||||
} from '../../queries/TimeQueries'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||
import { getSafeBottom } from '../../utils/SafeAreaUtils'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
@@ -60,6 +60,12 @@ const TimerDetails = () => {
|
||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||
useCircleMembers()
|
||||
|
||||
// Timer hooks
|
||||
const { data: choreTimer, refetch: refetchTimer } = useChoreTimer(choreId)
|
||||
const startChore = useStartChore()
|
||||
const pauseChore = usePauseChore()
|
||||
const updateTimeSession = useUpdateTimeSession()
|
||||
|
||||
const members = circleMembersData?.res || []
|
||||
|
||||
// Helper function to find member by user ID
|
||||
@@ -75,13 +81,12 @@ const TimerDetails = () => {
|
||||
checkTouchDevice()
|
||||
}, [])
|
||||
|
||||
// Fetch timer data when component mounts
|
||||
// Update timerData when choreTimer data changes
|
||||
useEffect(() => {
|
||||
if (choreId) {
|
||||
fetchTimerData()
|
||||
if (choreTimer?.res) {
|
||||
setTimerData(choreTimer.res)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [choreId])
|
||||
}, [choreTimer])
|
||||
|
||||
// Real-time update interval for active timers
|
||||
useEffect(() => {
|
||||
@@ -97,28 +102,6 @@ const TimerDetails = () => {
|
||||
}
|
||||
}, [timerData])
|
||||
|
||||
const fetchTimerData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await GetChoreTimer(choreId)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setTimerData(data.res)
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to fetch timer data',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error fetching timer data',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = seconds => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
@@ -236,20 +219,25 @@ const TimerDetails = () => {
|
||||
pauseLog: editingData.pauseLog,
|
||||
}
|
||||
|
||||
const response = await UpdateTimeSession(choreId, sessionId, updateData)
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
cancelEditingSession(sessionId)
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
updateTimeSession.mutate(
|
||||
{ choreId, sessionId, sessionData: updateData },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
})
|
||||
refetchTimer()
|
||||
cancelEditingSession(sessionId)
|
||||
},
|
||||
onError: () => {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error updating session',
|
||||
@@ -261,56 +249,48 @@ const TimerDetails = () => {
|
||||
}
|
||||
|
||||
// Timer control functions
|
||||
const handleStartTimer = async () => {
|
||||
const handleStartTimer = () => {
|
||||
setTimerActionLoading(true)
|
||||
try {
|
||||
const response = await StartChore(choreId)
|
||||
if (response.ok) {
|
||||
startChore.mutate(choreId, {
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Timer Started',
|
||||
message: 'Work session has been started successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
} else {
|
||||
refetchTimer()
|
||||
},
|
||||
onError: () => {
|
||||
showError({
|
||||
title: 'Failed to start timer',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error starting timer',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setTimerActionLoading(false)
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
setTimerActionLoading(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handlePauseTimer = async () => {
|
||||
const handlePauseTimer = () => {
|
||||
setTimerActionLoading(true)
|
||||
try {
|
||||
const response = await PauseChore(choreId)
|
||||
if (response.ok) {
|
||||
pauseChore.mutate(choreId, {
|
||||
onSuccess: () => {
|
||||
showSuccess({
|
||||
title: 'Timer Paused',
|
||||
message: 'Work session has been paused.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
} else {
|
||||
refetchTimer()
|
||||
},
|
||||
onError: () => {
|
||||
showError({
|
||||
title: 'Failed to pause timer',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error pausing timer',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setTimerActionLoading(false)
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
setTimerActionLoading(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Determine if timer is currently running
|
||||
|
||||
@@ -26,12 +26,11 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
|
||||
import {
|
||||
ArchiveChore,
|
||||
DeleteChore,
|
||||
SkipChore,
|
||||
UnArchiveChore,
|
||||
UpdateDueDate,
|
||||
} from '../../utils/Fetcher'
|
||||
import { useArchiveChore, useUnArchiveChore } from '../../queries/ChoreQueries'
|
||||
|
||||
const ChoreActionMenu = ({
|
||||
chore,
|
||||
@@ -55,6 +54,8 @@ const ChoreActionMenu = ({
|
||||
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(() => {
|
||||
@@ -126,22 +127,18 @@ const ChoreActionMenu = ({
|
||||
|
||||
const handleArchive = () => {
|
||||
if (chore.isActive) {
|
||||
ArchiveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(() => {
|
||||
const newChore = { ...chore, isActive: false }
|
||||
onChoreUpdate?.(newChore, 'archive')
|
||||
})
|
||||
}
|
||||
archiveChore.mutate(chore.id, {
|
||||
onSuccess: () => {
|
||||
const newChore = { ...chore, isActive: false }
|
||||
onChoreUpdate?.(newChore, 'archive')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
UnArchiveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(() => {
|
||||
const newChore = { ...chore, isActive: true }
|
||||
onChoreUpdate?.(newChore, 'unarchive')
|
||||
})
|
||||
}
|
||||
unArchiveChore.mutate(chore.id, {
|
||||
onSuccess: () => {
|
||||
const newChore = { ...chore, isActive: true }
|
||||
onChoreUpdate?.(newChore, 'unarchive')
|
||||
},
|
||||
})
|
||||
}
|
||||
handleMenuClose()
|
||||
|
||||
Reference in New Issue
Block a user