feat: enhance chore management with history and sync improvements
- Added support for starting and pausing chores with offline handling. - Implemented chore history updates and deletions with pending command management. - Enhanced sync engine to handle chore history changes and deletions. - Introduced a PendingBadge component to display pending actions for chore history. - Updated DeveloperSettings to include sync diagnostics and reset functionality. - Improved user notifications for offline actions and pending commands. - Refactored ChoreView to dynamically generate info cards based on chore history. - Added sync status indicator with cancel all functionality for pending commands.
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Refresh, Token } from '@mui/icons-material'
|
||||
import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { networkManager } from '../../hooks/NetworkManager'
|
||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||
import { useSSEContext } from '../../hooks/useSSEContext'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
import { commandQueue } from '../../utils/CommandQueue'
|
||||
import { RefreshToken } from '../../utils/Fetcher'
|
||||
import { offlineDB } from '../../utils/OfflineDB'
|
||||
import { syncEngine } from '../../utils/SyncEngine'
|
||||
import { getRefreshTokenExpiry, isNative } from '../../utils/TokenStorage'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
|
||||
const DeveloperSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
||||
const {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
@@ -31,9 +40,48 @@ const DeveloperSettings = () => {
|
||||
const [isRefreshingDirect, setIsRefreshingDirect] = useState(false)
|
||||
const [scheduledNotifications, setScheduledNotifications] = useState([])
|
||||
const [isLoadingNotifications, setIsLoadingNotifications] = useState(false)
|
||||
const [isResettingSync, setIsResettingSync] = useState(false)
|
||||
const [syncDiagnostics, setSyncDiagnostics] = useState({
|
||||
cursor: null,
|
||||
lastSync: null,
|
||||
pendingCount: 0,
|
||||
failedCount: 0,
|
||||
syncing: false,
|
||||
syncError: null,
|
||||
isOnline: networkManager.isOnline,
|
||||
isNetworkOn: networkManager.isNetworkOn,
|
||||
offlineSince: networkManager.offlineSince,
|
||||
lastChecked: networkManager.lastChecked,
|
||||
})
|
||||
|
||||
const { showNotification } = useNotification()
|
||||
|
||||
const refreshSyncDiagnostics = useCallback(async () => {
|
||||
try {
|
||||
const [cursor, lastSync, pendingCommands, failedCommands] =
|
||||
await Promise.all([
|
||||
offlineDB.getSyncCursor(),
|
||||
offlineDB.getLastSyncTime(),
|
||||
commandQueue.getPending(),
|
||||
commandQueue.getFailed(),
|
||||
])
|
||||
|
||||
setSyncDiagnostics(prev => ({
|
||||
...prev,
|
||||
cursor,
|
||||
lastSync,
|
||||
pendingCount: pendingCommands.length,
|
||||
failedCount: failedCommands.length,
|
||||
isOnline: networkManager.isOnline,
|
||||
isNetworkOn: networkManager.isNetworkOn,
|
||||
offlineSince: networkManager.offlineSince,
|
||||
lastChecked: networkManager.lastChecked,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to load sync diagnostics:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setIsNativePlatform(isNative())
|
||||
|
||||
@@ -54,12 +102,8 @@ const DeveloperSettings = () => {
|
||||
const pending = await LocalNotifications.getPending()
|
||||
// Sort by schedule time (earliest first)
|
||||
const sorted = pending.notifications.sort((a, b) => {
|
||||
const timeA = a.schedule?.at
|
||||
? new Date(a.schedule.at).getTime()
|
||||
: 0
|
||||
const timeB = b.schedule?.at
|
||||
? new Date(b.schedule.at).getTime()
|
||||
: 0
|
||||
const timeA = a.schedule?.at ? new Date(a.schedule.at).getTime() : 0
|
||||
const timeB = b.schedule?.at ? new Date(b.schedule.at).getTime() : 0
|
||||
return timeA - timeB
|
||||
})
|
||||
setScheduledNotifications(sorted)
|
||||
@@ -73,7 +117,39 @@ const DeveloperSettings = () => {
|
||||
|
||||
loadTokenData()
|
||||
loadScheduledNotifications()
|
||||
}, [])
|
||||
refreshSyncDiagnostics()
|
||||
}, [refreshSyncDiagnostics])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeSync = syncEngine.onSyncStateChange(state => {
|
||||
setSyncDiagnostics(prev => ({
|
||||
...prev,
|
||||
syncing:
|
||||
typeof state.syncing === 'boolean' ? state.syncing : prev.syncing,
|
||||
syncError: state.error ?? prev.syncError,
|
||||
lastSync: state.lastSync ?? prev.lastSync,
|
||||
}))
|
||||
})
|
||||
|
||||
networkManager.registerNetworkListener(() => {
|
||||
setSyncDiagnostics(prev => ({
|
||||
...prev,
|
||||
isOnline: networkManager.isOnline,
|
||||
isNetworkOn: networkManager.isNetworkOn,
|
||||
offlineSince: networkManager.offlineSince,
|
||||
lastChecked: networkManager.lastChecked,
|
||||
}))
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refreshSyncDiagnostics()
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
unsubscribeSync()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [refreshSyncDiagnostics])
|
||||
|
||||
useEffect(() => {
|
||||
const calculateTimeLeft = () => {
|
||||
@@ -239,6 +315,51 @@ const DeveloperSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetDatabaseAndResync = async () => {
|
||||
showConfirmation(
|
||||
'This will clear local offline data and pending commands, then start a full sync from the beginning. Continue?',
|
||||
'Clear Local DB & Re-Sync',
|
||||
async () => {
|
||||
setIsResettingSync(true)
|
||||
try {
|
||||
await offlineDB.clearAll()
|
||||
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Local offline database cleared. Starting full sync...',
|
||||
})
|
||||
|
||||
const didSync = await syncEngine.sync()
|
||||
if (didSync) {
|
||||
await queryClient.invalidateQueries()
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Full sync completed from the beginning',
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Database cleared. Full sync did not run (likely offline or already syncing).',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset database and resync:', error)
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Reset/resync failed: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
await refreshSyncDiagnostics()
|
||||
setIsResettingSync(false)
|
||||
}
|
||||
},
|
||||
'Clear & Re-Sync',
|
||||
'Cancel',
|
||||
'danger',
|
||||
)
|
||||
}
|
||||
|
||||
const getNotificationStatusColor = scheduleTime => {
|
||||
if (!scheduleTime) return 'neutral'
|
||||
|
||||
@@ -252,6 +373,11 @@ const DeveloperSettings = () => {
|
||||
return 'success' // More than 1 hour
|
||||
}
|
||||
|
||||
const formatDateTime = timestamp => {
|
||||
if (!timestamp) return 'N/A'
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-4 py-4' id='developer'>
|
||||
<Typography level='h3'>Developer Settings</Typography>
|
||||
@@ -377,6 +503,143 @@ const DeveloperSettings = () => {
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<Card variant='outlined'>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography level='title-lg'>Sync & Network Diagnostics</Typography>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='soft'
|
||||
startDecorator={<Refresh />}
|
||||
onClick={refreshSyncDiagnostics}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='title-sm'>Network Status</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Connection:{' '}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={syncDiagnostics.isOnline ? 'success' : 'danger'}
|
||||
>
|
||||
{syncDiagnostics.isOnline ? 'Online' : 'Offline'}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Device Network:{' '}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={
|
||||
syncDiagnostics.isNetworkOn === false
|
||||
? 'danger'
|
||||
: syncDiagnostics.isNetworkOn === true
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
}
|
||||
>
|
||||
{syncDiagnostics.isNetworkOn === false
|
||||
? 'Disconnected'
|
||||
: syncDiagnostics.isNetworkOn === true
|
||||
? 'Connected'
|
||||
: 'Unknown'}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Offline Since: {formatDateTime(syncDiagnostics.offlineSince)}
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Last Network Check: {formatDateTime(syncDiagnostics.lastChecked)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='title-sm'>Sync Offset Information</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Sync Cursor:{' '}
|
||||
<Chip size='sm' variant='soft'>
|
||||
{syncDiagnostics.cursor ?? 'N/A'}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Last Sync:{' '}
|
||||
<Chip size='sm' variant='soft' color='primary'>
|
||||
{formatDateTime(syncDiagnostics.lastSync)}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Sync State:{' '}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={syncDiagnostics.syncing ? 'warning' : 'success'}
|
||||
>
|
||||
{syncDiagnostics.syncing ? 'Syncing' : 'Idle'}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Pending Commands:{' '}
|
||||
<Chip size='sm' variant='soft' color='warning'>
|
||||
{syncDiagnostics.pendingCount}
|
||||
</Chip>
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Failed Commands:{' '}
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={syncDiagnostics.failedCount > 0 ? 'danger' : 'success'}
|
||||
>
|
||||
{syncDiagnostics.failedCount}
|
||||
</Chip>
|
||||
</Typography>
|
||||
{syncDiagnostics.syncError && (
|
||||
<Typography level='body-sm' color='danger'>
|
||||
Sync Error: {syncDiagnostics.syncError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='title-sm'>Recovery Actions</Typography>
|
||||
<Typography level='body-xs' color='warning'>
|
||||
Clears local offline cache, sync cursor, and queued commands, then
|
||||
re-syncs from the beginning.
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='soft'
|
||||
onClick={handleResetDatabaseAndResync}
|
||||
loading={isResettingSync}
|
||||
disabled={isResettingSync || syncDiagnostics.syncing}
|
||||
>
|
||||
Clear DB & Full Re-Sync
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{isNativePlatform && (
|
||||
<Card variant='outlined'>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
@@ -436,9 +699,7 @@ const DeveloperSettings = () => {
|
||||
? new Date(scheduleTime)
|
||||
: null
|
||||
const now = new Date()
|
||||
const timeUntil = scheduledDate
|
||||
? scheduledDate - now
|
||||
: null
|
||||
const timeUntil = scheduledDate ? scheduledDate - now : null
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -726,6 +987,8 @@ const DeveloperSettings = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<ConfirmationModal config={confirmModalConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user