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'} {Priorities.map((priority, index) => ( { handleUpdatePriority(priority) }} color={priority.color} > {priority.icon} {priority.name} ))} { handleUpdatePriority({ name: 'No Priority', value: 0, }) setChorePriority(null) }} > 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() ? ( <> ) : ( ) ) : ( // Normal completion flow <> )} {/* 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