Merge branch 'dev'
This commit is contained in:
@@ -19,6 +19,14 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deep link intent filter for OAuth callback -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="donetick" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|||||||
@@ -42,10 +42,16 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string></string>
|
<string>com.donetick.app</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>donetick</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
|||||||
@@ -1,10 +1,44 @@
|
|||||||
import { App as mobileApp } from '@capacitor/app'
|
import { App as mobileApp } from '@capacitor/app'
|
||||||
|
import { Browser } from '@capacitor/browser'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { Device } from '@capacitor/device'
|
import { Device } from '@capacitor/device'
|
||||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
import { PushNotifications } from '@capacitor/push-notifications'
|
import { PushNotifications } from '@capacitor/push-notifications'
|
||||||
import { RegisterDeviceToken } from './utils/Fetcher'
|
import { RegisterDeviceToken } from './utils/Fetcher'
|
||||||
|
|
||||||
|
// OAuth callback handler for deep links
|
||||||
|
const handleOAuthDeepLink = async url => {
|
||||||
|
console.log('OAuth deep link received:', url)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the URL to extract code and state
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
const code = urlObj.searchParams.get('code')
|
||||||
|
const state = urlObj.searchParams.get('state')
|
||||||
|
|
||||||
|
if (code && state) {
|
||||||
|
// Store the OAuth params for the app to pick up
|
||||||
|
await Preferences.set({
|
||||||
|
key: 'oauth_callback',
|
||||||
|
value: JSON.stringify({ code, state, timestamp: Date.now() }),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the browser if it's still open
|
||||||
|
try {
|
||||||
|
await Browser.close()
|
||||||
|
} catch (e) {
|
||||||
|
// Browser might already be closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the OAuth handler page
|
||||||
|
window.location.href = `/auth/oauth2?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling OAuth deep link:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const localNotificationListenerRegistration = () => {
|
const localNotificationListenerRegistration = () => {
|
||||||
LocalNotifications.addListener('localNotificationReceived', notification => {
|
LocalNotifications.addListener('localNotificationReceived', notification => {
|
||||||
console.log('Notification received', notification)
|
console.log('Notification received', notification)
|
||||||
@@ -180,6 +214,17 @@ const registerCapacitorListeners = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
localNotificationListenerRegistration()
|
localNotificationListenerRegistration()
|
||||||
|
|
||||||
|
// Register deep link handler for OAuth and other deep links
|
||||||
|
mobileApp.addListener('appUrlOpen', event => {
|
||||||
|
console.log('App URL opened:', event.url)
|
||||||
|
|
||||||
|
// Handle OAuth callback
|
||||||
|
if (event.url.startsWith('donetick://auth/')) {
|
||||||
|
handleOAuthDeepLink(event.url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mobileApp.addListener('backButton', ({ canGoBack }) => {
|
mobileApp.addListener('backButton', ({ canGoBack }) => {
|
||||||
if (canGoBack) {
|
if (canGoBack) {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import Settings from '@/views/Settings/Settings'
|
|||||||
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
||||||
import SettingsRoutes from '@/views/Settings/SettingsRoutes'
|
import SettingsRoutes from '@/views/Settings/SettingsRoutes'
|
||||||
import ThemeSettings from '@/views/Settings/ThemeSettings'
|
import ThemeSettings from '@/views/Settings/ThemeSettings'
|
||||||
import { Capacitor } from '@capacitor/core'
|
|
||||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||||
import AuthenticationLoading from '../views/Authorization/Authenticating'
|
import AuthenticationLoading from '../views/Authorization/Authenticating'
|
||||||
import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
|
import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
|
||||||
@@ -46,8 +45,9 @@ import UserActivities from '../views/User/UserActivities'
|
|||||||
import UserPoints from '../views/User/UserPoints'
|
import UserPoints from '../views/User/UserPoints'
|
||||||
const getMainRoute = () => {
|
const getMainRoute = () => {
|
||||||
if (
|
if (
|
||||||
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true' &&
|
// if domain is www.donetick.com or donetick.com then show landing page:
|
||||||
!Capacitor.isNativePlatform()
|
window.location.hostname === 'www.donetick.com' ||
|
||||||
|
window.location.hostname === 'donetick.com'
|
||||||
) {
|
) {
|
||||||
return <Landing />
|
return <Landing />
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ const Router = createBrowserRouter([
|
|||||||
element: <AuthenticationLoading />,
|
element: <AuthenticationLoading />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/landing',
|
path: '/welcome',
|
||||||
element: <Landing />,
|
element: <Landing />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Logo from '../../Logo'
|
import Logo from '../../Logo'
|
||||||
import { apiClient } from '../../utils/ApiClient'
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
|
import { apiClient } from '../../utils/ApiClient'
|
||||||
import { GetUserProfile } from '../../utils/Fetcher'
|
import { GetUserProfile } from '../../utils/Fetcher'
|
||||||
|
|
||||||
const AuthenticationLoading = () => {
|
const AuthenticationLoading = () => {
|
||||||
@@ -58,8 +58,9 @@ const AuthenticationLoading = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
const baseURL = apiClient.baseURL
|
const baseURL = apiClient.getApiURL()
|
||||||
fetch(`${baseURL}/auth/${provider}/callback`, {
|
|
||||||
|
fetch(`${baseURL}/auth/oauth2/callback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -67,6 +68,7 @@ const AuthenticationLoading = () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
code,
|
code,
|
||||||
state: returnedState,
|
state: returnedState,
|
||||||
|
redirect_uri: `${window.location.origin}/auth/oauth2`,
|
||||||
}),
|
}),
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
|||||||
@@ -182,11 +182,19 @@ const LoginView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(`/auth/${provider}/callback`, {
|
const response = await apiClient.post(
|
||||||
provider: provider,
|
`/auth/${provider}/callback`,
|
||||||
token: getAccessToken(data),
|
JSON.stringify({
|
||||||
data: data,
|
provider: provider,
|
||||||
})
|
token: getAccessToken(data),
|
||||||
|
data: data,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseData = await response.json()
|
const responseData = await response.json()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
CalendarMonth,
|
CalendarMonth,
|
||||||
CancelScheduleSend,
|
|
||||||
Check,
|
Check,
|
||||||
Checklist,
|
Checklist,
|
||||||
CloseFullscreen,
|
CloseFullscreen,
|
||||||
@@ -35,7 +34,6 @@ import {
|
|||||||
MenuButton,
|
MenuButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Sheet,
|
Sheet,
|
||||||
Snackbar,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { Divider } from '@mui/material'
|
import { Divider } from '@mui/material'
|
||||||
@@ -54,6 +52,7 @@ import {
|
|||||||
useStartChore,
|
useStartChore,
|
||||||
} from '../../queries/TimeQueries'
|
} from '../../queries/TimeQueries'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||||
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
|
import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
import {
|
import {
|
||||||
@@ -63,6 +62,7 @@ import {
|
|||||||
RejectChore,
|
RejectChore,
|
||||||
SkipChore,
|
SkipChore,
|
||||||
UnArchiveChore,
|
UnArchiveChore,
|
||||||
|
UndoChoreAction,
|
||||||
UpdateChorePriority,
|
UpdateChorePriority,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import Priorities from '../../utils/Priorities'
|
import Priorities from '../../utils/Priorities'
|
||||||
@@ -82,12 +82,11 @@ const ChoreView = () => {
|
|||||||
const { choreId } = useParams()
|
const { choreId } = useParams()
|
||||||
const [note, setNote] = useState(null)
|
const [note, setNote] = useState(null)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const { showSuccess, showError, showUndo } = useNotification()
|
||||||
|
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
const [isPendingCompletion, setIsPendingCompletion] = useState(false)
|
|
||||||
const [timeoutId, setTimeoutId] = useState(null)
|
const [timeoutId, setTimeoutId] = useState(null)
|
||||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
|
|
||||||
const [completedDate, setCompletedDate] = useState(null)
|
const [completedDate, setCompletedDate] = useState(null)
|
||||||
const [confirmModelConfig, setConfirmModelConfig] = useState({
|
const [confirmModelConfig, setConfirmModelConfig] = useState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
@@ -189,20 +188,7 @@ const ChoreView = () => {
|
|||||||
setInfoCards(cards)
|
setInfoCards(cards)
|
||||||
}
|
}
|
||||||
const handleTaskCompletion = () => {
|
const handleTaskCompletion = () => {
|
||||||
setIsPendingCompletion(true)
|
let id = setTimeout(() => {
|
||||||
let seconds = 3 // Starting countdown from 3 seconds
|
|
||||||
setSecondsLeftToCancel(seconds)
|
|
||||||
|
|
||||||
const countdownInterval = setInterval(() => {
|
|
||||||
seconds -= 1
|
|
||||||
setSecondsLeftToCancel(seconds)
|
|
||||||
|
|
||||||
if (seconds <= 0) {
|
|
||||||
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
MarkChoreComplete(
|
MarkChoreComplete(
|
||||||
choreId,
|
choreId,
|
||||||
impersonatedUser
|
impersonatedUser
|
||||||
@@ -220,11 +206,8 @@ const ChoreView = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsPendingCompletion(false)
|
|
||||||
clearTimeout(id)
|
clearTimeout(id)
|
||||||
clearInterval(countdownInterval) // Ensure to clear this interval as well
|
|
||||||
setTimeoutId(null)
|
setTimeoutId(null)
|
||||||
setSecondsLeftToCancel(null)
|
|
||||||
// Invalidate chores cache to refetch data
|
// Invalidate chores cache to refetch data
|
||||||
queryClient.invalidateQueries(['chores'])
|
queryClient.invalidateQueries(['chores'])
|
||||||
})
|
})
|
||||||
@@ -241,6 +224,16 @@ const ChoreView = () => {
|
|||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
setTimeoutId(id)
|
setTimeoutId(id)
|
||||||
|
|
||||||
|
// Show undo notification
|
||||||
|
showSuccess({
|
||||||
|
title: 'Task Completed',
|
||||||
|
message: 'Your task has been marked as complete',
|
||||||
|
undoAction: () => {
|
||||||
|
clearTimeout(id)
|
||||||
|
setTimeoutId(null)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const handleSkippingTask = () => {
|
const handleSkippingTask = () => {
|
||||||
SkipChore(choreId).then(response => {
|
SkipChore(choreId).then(response => {
|
||||||
@@ -250,6 +243,36 @@ const ChoreView = () => {
|
|||||||
setChore(newChore)
|
setChore(newChore)
|
||||||
// Invalidate chores cache to refetch data
|
// Invalidate chores cache to refetch data
|
||||||
queryClient.invalidateQueries(['chores'])
|
queryClient.invalidateQueries(['chores'])
|
||||||
|
|
||||||
|
// Show undo notification
|
||||||
|
showSuccess({
|
||||||
|
message: 'Task skipped',
|
||||||
|
undoAction: async () => {
|
||||||
|
try {
|
||||||
|
const undoResponse = await UndoChoreAction(choreId)
|
||||||
|
if (undoResponse.ok) {
|
||||||
|
// Refetch chore details after undo
|
||||||
|
const detailResponse = await GetChoreDetailById(choreId)
|
||||||
|
if (detailResponse.ok) {
|
||||||
|
const detailData = await detailResponse.json()
|
||||||
|
setChore(detailData.res)
|
||||||
|
queryClient.invalidateQueries(['chores'])
|
||||||
|
}
|
||||||
|
showUndo({
|
||||||
|
title: 'Undo Successful',
|
||||||
|
message: 'Task skip has been undone.',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to undo')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError({
|
||||||
|
title: 'Undo Failed',
|
||||||
|
message: 'Unable to undo the action. Please try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -924,12 +947,11 @@ const ChoreView = () => {
|
|||||||
size='lg'
|
size='lg'
|
||||||
onClick={handleTaskCompletion}
|
onClick={handleTaskCompletion}
|
||||||
disabled={
|
disabled={
|
||||||
isPendingCompletion ||
|
|
||||||
notInCompletionWindow(chore) ||
|
notInCompletionWindow(chore) ||
|
||||||
(chore.lastCompletedDate !== null &&
|
(chore.lastCompletedDate !== null &&
|
||||||
chore.frequencyType === 'once')
|
chore.frequencyType === 'once')
|
||||||
}
|
}
|
||||||
color={isPendingCompletion ? 'danger' : 'success'}
|
color='success'
|
||||||
startDecorator={<Check />}
|
startDecorator={<Check />}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 4,
|
flex: 4,
|
||||||
@@ -1017,31 +1039,6 @@ const ChoreView = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Snackbar
|
|
||||||
open={isPendingCompletion}
|
|
||||||
endDecorator={
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
setIsPendingCompletion(false)
|
|
||||||
setTimeoutId(null)
|
|
||||||
setSecondsLeftToCancel(null) // Reset or adjust as needed
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size='lg'
|
|
||||||
variant='outlined'
|
|
||||||
color='danger'
|
|
||||||
startDecorator={<CancelScheduleSend />}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Typography level='body-md' textAlign={'center'}>
|
|
||||||
Task will be marked as completed in {secondsLeftToCancel} seconds
|
|
||||||
</Typography>
|
|
||||||
</Snackbar>
|
|
||||||
<ConfirmationModal config={confirmModelConfig} />
|
<ConfirmationModal config={confirmModelConfig} />
|
||||||
<ConfirmationModal config={timerActionConfig} />
|
<ConfirmationModal config={timerActionConfig} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { LocalNotifications } from '@capacitor/local-notifications'
|
|||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
import murmurhash from 'murmurhash'
|
import murmurhash from 'murmurhash'
|
||||||
|
|
||||||
|
const MAX_LOCAL_NOTIFICATIONS = 64
|
||||||
|
|
||||||
const getNotificationPreferences = async () => {
|
const getNotificationPreferences = async () => {
|
||||||
const ret = await Preferences.get({ key: 'notificationPreferences' })
|
const ret = await Preferences.get({ key: 'notificationPreferences' })
|
||||||
return JSON.parse(ret.value)
|
return JSON.parse(ret.value)
|
||||||
@@ -206,6 +208,13 @@ const scheduleChoreNotification = async (
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// sort from soonest to latest:
|
||||||
|
notifications.sort((a, b) => a.schedule.at - b.schedule.at)
|
||||||
|
|
||||||
|
// cap it for 64 notifications for Android:
|
||||||
|
if (notifications.length > MAX_LOCAL_NOTIFICATIONS) {
|
||||||
|
notifications.splice(MAX_LOCAL_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
LocalNotifications.schedule({
|
LocalNotifications.schedule({
|
||||||
notifications,
|
notifications,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useArchiveChore, useDeleteChores } from '../../../queries/ChoreQueries'
|
import { useCallback } from 'react'
|
||||||
|
import { useArchiveChore } from '../../../queries/ChoreQueries'
|
||||||
import { usePauseChore, useStartChore } from '../../../queries/TimeQueries'
|
import { usePauseChore, useStartChore } from '../../../queries/TimeQueries'
|
||||||
import {
|
import {
|
||||||
ApproveChore,
|
ApproveChore,
|
||||||
DeleteChore,
|
DeleteChore,
|
||||||
MarkChoreComplete,
|
MarkChoreComplete,
|
||||||
NudgeChore,
|
NudgeChore,
|
||||||
RejectChore,
|
RejectChore,
|
||||||
SkipChore,
|
SkipChore,
|
||||||
UndoChoreAction,
|
UndoChoreAction,
|
||||||
UpdateChoreAssignee,
|
UpdateChoreAssignee,
|
||||||
UpdateDueDate,
|
UpdateDueDate,
|
||||||
} from '../../../utils/Fetcher'
|
} from '../../../utils/Fetcher'
|
||||||
|
|
||||||
export const useChoreActions = ({
|
export const useChoreActions = ({
|
||||||
@@ -682,6 +682,23 @@ export const useChoreActions = ({
|
|||||||
showSuccess({
|
showSuccess({
|
||||||
title: '⏭️ Tasks Skipped',
|
title: '⏭️ Tasks Skipped',
|
||||||
message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
|
message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
|
||||||
|
undoAction: async () => {
|
||||||
|
try {
|
||||||
|
for (const chore of skippedTasks) {
|
||||||
|
await UndoChoreAction(chore.id)
|
||||||
|
}
|
||||||
|
refetchChores()
|
||||||
|
showUndo({
|
||||||
|
title: 'Undo Successful',
|
||||||
|
message: `Undo skip for ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
showError({
|
||||||
|
title: 'Undo Failed',
|
||||||
|
message: 'Unable to undo the action. Please try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +721,7 @@ export const useChoreActions = ({
|
|||||||
setConfirmModelConfig({})
|
setConfirmModelConfig({})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [getSelectedChoresData, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
}, [getSelectedChoresData, showSuccess, showError, showUndo, refetchChores, clearSelection, setConfirmModelConfig])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleChoreAction,
|
handleChoreAction,
|
||||||
|
|||||||
Reference in New Issue
Block a user