import {
CalendarMonth,
CancelScheduleSend,
Check,
Checklist,
CloseFullscreen,
Edit,
History,
HourglassEmpty,
LowPriority,
OpenInFull,
PeopleAlt,
Person,
PlayArrow,
SwitchAccessShortcut,
ThumbDown,
ThumbUp,
} from '@mui/icons-material'
import {
Box,
Button,
Card,
CardContent,
Checkbox,
Chip,
Container,
Dropdown,
FormControl,
Grid,
IconButton,
Input,
Menu,
MenuButton,
MenuItem,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import { Divider } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
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 Priorities from '../../utils/Priorities'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import LoadingComponent from '../components/Loading.jsx'
import RichTextEditor from '../components/RichTextEditor.jsx'
import SubTasks from '../components/SubTask.jsx'
import TimePassedCard from './TimePassedCard.jsx'
import TimerSplitButton from './TimerSplitButton.jsx'
const ChoreView = () => {
const [chore, setChore] = useState({})
const navigate = useNavigate()
const [performers, setPerformers] = useState([])
const [infoCards, setInfoCards] = useState([])
const { choreId } = useParams()
const [note, setNote] = useState(null)
const queryClient = useQueryClient()
const [searchParams] = useSearchParams()
const [isPendingCompletion, setIsPendingCompletion] = useState(false)
const [timeoutId, setTimeoutId] = useState(null)
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
const [completedDate, setCompletedDate] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({})
const [chorePriority, setChorePriority] = useState(null)
const [isDescriptionOpen, setIsDescriptionOpen] = useState(false)
const [timerActionConfig, setTimerActionConfig] = useState({})
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
const { data: userProfile } = useUserProfile()
const { impersonatedUser } = useImpersonateUser()
const { data: choreData, isLoading: isChoreLoading } =
useChoreDetails(choreId)
useEffect(() => {
if (!choreData || !choreData.res || !circleMembersData) {
return
}
setChore(choreData.res)
setChorePriority(Priorities.find(p => p.value === choreData.res.priority))
document.title = 'Donetick: ' + choreData.res.name
setPerformers(circleMembersData.res)
const auto_complete = searchParams.get('auto_complete')
if (auto_complete === 'true') {
handleTaskCompletion()
}
}, [choreData, circleMembersData])
useEffect(() => {
if (chore && performers?.length > 0) {
generateInfoCards(chore)
}
}, [chore, performers])
const handleUpdatePriority = priority => {
UpdateChorePriority(choreId, priority.value).then(response => {
if (response.ok) {
response.json().then(() => {
setChorePriority(priority)
// Invalidate chores cache to refetch data
queryClient.invalidateQueries(['chores'])
})
}
})
}
const generateInfoCards = chore => {
const cards = [
{
size: 6,
icon: ,
title: 'Assignment',
text: `Assigned: ${
performers.find(p => p.userId === chore.assignedTo)?.displayName ||
'N/A'
}`,
subtext: ` Last: ${
chore.lastCompletedDate
? performers.find(p => p.userId === chore.lastCompletedBy)
?.displayName
: '--'
}`,
},
{
size: 6,
icon: ,
title: 'Schedule',
text: `Due: ${
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : 'N/A'
}`,
subtext: `Last: ${
chore.lastCompletedDate
? moment(chore.lastCompletedDate).fromNow()
: 'N/A'
}`,
},
{
size: 6,
icon: ,
title: 'Statistics',
text: `Completed: ${chore.totalCompletedCount || 0} times`,
},
{
size: 6,
icon: ,
title: 'Details',
subtext: `Created By: ${
performers.find(p => p.userId === chore.createdBy)?.displayName ||
'N/A'
}`,
},
]
setInfoCards(cards)
}
const handleTaskCompletion = () => {
setIsPendingCompletion(true)
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(
choreId,
impersonatedUser
? { completedBy: impersonatedUser.userId, note }
: { note },
completedDate,
null,
)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {
setNote(null)
setChore(data.res)
})
}
})
.then(() => {
setIsPendingCompletion(false)
clearTimeout(id)
clearInterval(countdownInterval) // Ensure to clear this interval as well
setTimeoutId(null)
setSecondsLeftToCancel(null)
// Invalidate chores cache to refetch data
queryClient.invalidateQueries(['chores'])
})
.then(() => {
// refetch the chore details
GetChoreDetailById(choreId).then(resp => {
if (resp.ok) {
return resp.json().then(data => {
setChore(data.res)
})
}
})
})
}, 3000)
setTimeoutId(id)
}
const handleSkippingTask = () => {
SkipChore(choreId).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
setChore(newChore)
// Invalidate chores cache to refetch data
queryClient.invalidateQueries(['chores'])
})
}
})
}
const handleChoreStart = () => {
StartChore(choreId).then(response => {
if (response.ok) {
response.json().then(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)
})
}
})
}
const handleResetTimer = () => {
setTimerActionConfig({
isOpen: true,
title: 'Reset Timer',
message:
'Are you sure you want to reset the timer? This will clear all time records since you started the task.',
confirmText: 'Reset Timer',
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'])
})
}
})
}
setTimerActionConfig({})
},
})
}
const handleClearAllTime = () => {
setTimerActionConfig({
isOpen: true,
title: 'Clear All Time Records',
message:
'This will permanently delete all timers for this task and set it back to "not started".',
confirmText: 'Clear All Time',
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 => {
const newChore = {
...chore,
...data.res,
}
setChore(newChore)
queryClient.invalidateQueries(['chores'])
})
}
})
}
}
setTimerActionConfig({})
},
})
}
const handleApproveChore = () => {
ApproveChore(choreId).then(response => {
if (response.ok) {
response.json().then(data => {
setChore(data.res)
// Invalidate chores cache to refetch data
queryClient.invalidateQueries(['chores'])
})
}
})
}
const handleRejectChore = () => {
RejectChore(choreId).then(response => {
if (response.ok) {
response.json().then(data => {
setChore(data.res)
// Invalidate chores cache to refetch data
queryClient.invalidateQueries(['chores'])
})
}
})
}
// Check if the current user can approve/reject (admin, manager, or task owner)
const canApproveReject = () => {
if (!circleMembersData?.res || !chore) return false
const currentUser = circleMembersData.res.find(
member => member.userId === (impersonatedUser?.userId || userProfile?.id),
)
// User can approve/reject if they are:
// 1. Admin or manager of the circle
// 2. Owner/creator of the task
return (
currentUser?.role === 'admin' ||
currentUser?.role === 'manager' ||
chore.createdBy === (impersonatedUser?.userId || userProfile?.id)
)
}
if (isChoreLoading || isCircleMembersLoading) {
// while loading the chore or circle members, return a loading state
return
}
return (
{chore.name}
} size='md' sx={{ mb: 1 }}>
{chore.nextDueDate
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
: 'N/A'}
{chore?.labelsV2?.map((label, index) => (
{label?.name}
))}
{[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) && (
{
if (action === 'pause') {
handleChorePause()
} else if (action === 'resume') {
handleChoreStart()
}
}}
onShowDetails={() => navigate(`/chores/${choreId}/timer`)}
/>
)}
{infoCards.map((card, index) => (
{card.icon}
{card.title}
{card.text}
{card.subtext}
))}
{chorePriority ? chorePriority.icon : }
{chorePriority ? chorePriority.name : 'No Priority'}
{chore.description && (
<>
Description :
{
setIsDescriptionOpen(!isDescriptionOpen)
}}
size='sm'
sx={{
position: 'absolute',
bottom: 5,
right: 5,
}}
>
{isDescriptionOpen ? : }
>
)}
{chore.notes && (
<>
Previous note:
{chore.notes || '--'}
>
)}
{chore.subTasks && chore.subTasks.length > 0 && (
Subtasks :
{
setChore({
...chore,
subTasks: tasks,
})
}}
choreId={choreId}
/>
)}
Task Actions
{
if (e.target.checked) {
setNote('')
} else {
setNote(null)
}
}}
overlay
label={
Add a note
}
/>
{note !== null && (
Additional Notes:
)}
{
if (e.target.checked) {
setCompletedDate(
moment(new Date()).format('YYYY-MM-DDTHH:00:00'),
)
} else {
setCompletedDate(null)
}
}}
overlay
sx={
{
// my: 1,
}
}
label={
Set custom completion time
}
/>
{completedDate !== null && (
{
setCompletedDate(e.target.value)
}}
/>
)}
{chore.status === 3 ? (
// Pending approval: Show approve/reject for admins/managers/owners, grayed out button for others
canApproveReject() ? (
<>
}
sx={{
flex: 1,
}}
>
Approve
}
sx={{
flex: 1,
}}
>
Reject
>
) : (
}
>
Pending Approval
)
) : (
// Normal completion flow
<>
}
sx={{
flex: 4,
}}
>
Mark as done
>
)}
{/* Timer Button - Show split button when timer is active, regular button otherwise */}
{[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) ? (
{
if (action === 'pause') {
handleChorePause()
} else if (action === 'resume') {
handleChoreStart()
}
}}
onShowDetails={() => navigate(`/chores/${choreId}/timer`)}
onResetTimer={handleResetTimer}
onClearAllTime={handleClearAllTime}
fullWidth
/>
) : chore.status === ChoreStatus.PENDING_APPROVAL ? (
<>>
) : (
)}
{
if (timeoutId) {
clearTimeout(timeoutId)
setIsPendingCompletion(false)
setTimeoutId(null)
setSecondsLeftToCancel(null) // Reset or adjust as needed
}
}}
size='lg'
variant='outlined'
color='danger'
startDecorator={}
>
Cancel
}
>
Task will be marked as completed in {secondsLeftToCancel} seconds
)
}
export default ChoreView