import { Type as ListType, SwipeableList, SwipeableListItem, SwipeAction, TrailingActions, } from '@meauxt/react-swipeable-list' import '@meauxt/react-swipeable-list/dist/styles.css' import { AccessTime, Add, BrowseGallery, Delete, Edit, MoreVert, PauseCircle, Person, PlayArrow, } from '@mui/icons-material' import DeleteIcon from '@mui/icons-material/Delete' import EditIcon from '@mui/icons-material/Edit' import { Alert, Avatar, Box, Button, Card, CardContent, Chip, Container, FormControl, FormHelperText, Grid, IconButton, Input, Typography, } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import { useLocalization } from '../../contexts/LocalizationContext' import { useChoreTimer, usePauseChore, useStartChore, useUpdateTimeSession, } from '../../queries/TimeQueries' import { useCircleMembers } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { commandQueue, CommandType } from '../../utils/CommandQueue' import { isOfflineFeatureEnabled } from '../../utils/OfflineFeatureToggle' import { resolvePhotoURL } from '../../utils/Helpers' import { getSafeBottom } from '../../utils/SafeAreaUtils' import LoadingComponent from '../components/Loading' // Effectively "can this action be queued offline?" — requires the offline // feature, otherwise there is no command queue to replay it later. const isNetworkError = err => isOfflineFeatureEnabled() && err instanceof TypeError && err.message === 'Failed to fetch' const TimerDetails = () => { const { choreId } = useParams() const { fmt } = useLocalization() const [timerData, setTimerData] = useState(null) const [loading, setLoading] = useState(false) const [editingSessions, setEditingSessions] = useState({}) const [currentTime, setCurrentTime] = useState(new Date()) const [timerActionLoading, setTimerActionLoading] = useState(false) const [showMoreInfoId, setShowMoreInfoId] = useState(null) const { showError, showSuccess } = useNotification() // Fetch circle members data 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 const getMemberById = userId => { return members?.find(member => member.userId === userId) } // Update timerData when choreTimer data changes useEffect(() => { if (choreTimer?.res) { setTimerData(choreTimer.res) } }, [choreTimer]) // Real-time update interval for active timers useEffect(() => { let interval if (timerData && !timerData.endTime) { // Update every second if timer is active interval = setInterval(() => { setCurrentTime(new Date()) }, 1000) } return () => { if (interval) clearInterval(interval) } }, [timerData]) const formatTime = seconds => { const hours = Math.floor(seconds / 3600) const minutes = Math.floor((seconds % 3600) / 60) const secs = seconds % 60 return `${hours.toString().padStart(2, '0')}:${minutes .toString() .padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } const formatDuration = seconds => { if (seconds < 60) return `${seconds}s` if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s` const hours = Math.floor(seconds / 3600) const minutes = Math.floor((seconds % 3600) / 60) return `${hours}h ${minutes}m` } const startEditingSession = () => { if (timerData) { setEditingSessions(prev => ({ ...prev, [timerData.id]: { startTime: moment(timerData.startTime).format('YYYY-MM-DDTHH:mm:ss'), endTime: timerData.endTime ? moment(timerData.endTime).format('YYYY-MM-DDTHH:mm:ss') : '', duration: timerData.duration, formattedDuration: formatTime(timerData.duration), pauseLog: timerData.pauseLog || [], }, })) } } const addPauseLogEntry = sessionId => { setEditingSessions(prev => ({ ...prev, [sessionId]: { ...prev[sessionId], pauseLog: [ ...prev[sessionId].pauseLog, { start: new Date().toISOString(), end: null, duration: 0, updatedBy: 0, // This should be current user ID }, ], }, })) } const updatePauseLogEntry = (sessionId, pauseIndex, field, value) => { setEditingSessions(prev => { const updatedPauseLog = prev[sessionId].pauseLog.map((pause, index) => { if (index === pauseIndex) { const updatedPause = { ...pause, [field]: value } // Auto-calculate duration if both start and end are present if (updatedPause.start && updatedPause.end) { const startTime = new Date(updatedPause.start) const endTime = new Date(updatedPause.end) updatedPause.duration = Math.floor((endTime - startTime) / 1000) } return updatedPause } return pause }) return { ...prev, [sessionId]: { ...prev[sessionId], pauseLog: updatedPauseLog, }, } }) } const deletePauseLogEntry = (sessionId, pauseIndex) => { setEditingSessions(prev => ({ ...prev, [sessionId]: { ...prev[sessionId], pauseLog: prev[sessionId].pauseLog.filter( (_, index) => index !== pauseIndex, ), }, })) } const cancelEditingSession = sessionId => { setEditingSessions(prev => { // eslint-disable-next-line no-unused-vars const { [sessionId]: removed, ...rest } = prev return rest }) } const saveSession = async sessionId => { const editingData = editingSessions[sessionId] if (!editingData) return setLoading(true) try { // Use the auto-calculated duration from the editing session const updateData = { startTime: new Date(editingData.startTime).toISOString(), endTime: editingData.endTime ? new Date(editingData.endTime).toISOString() : null, duration: editingData.duration, pauseLog: editingData.pauseLog, } 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', message: error.message, }) } finally { setLoading(false) } } // Timer control functions const handleStartTimer = () => { setTimerActionLoading(true) startChore.mutate(choreId, { onSuccess: () => { showSuccess({ title: 'Timer Started', message: 'Work session has been started successfully.', }) refetchTimer() }, onError: async error => { if (isNetworkError(error)) { const cmdId = await commandQueue.enqueue( CommandType.START_CHORE, choreId, { id: choreId }, ) showSuccess({ title: 'Start queued', message: "You're offline — start will sync when back online", undoAction: async () => { await commandQueue.cancel(cmdId) }, }) return } showError({ title: 'Failed to start timer', message: 'Please try again.', }) }, onSettled: () => { setTimerActionLoading(false) }, }) } const handlePauseTimer = () => { setTimerActionLoading(true) pauseChore.mutate(choreId, { onSuccess: () => { showSuccess({ title: 'Timer Paused', message: 'Work session has been paused.', }) refetchTimer() }, onError: async error => { if (isNetworkError(error)) { const cmdId = await commandQueue.enqueue( CommandType.PAUSE_CHORE, choreId, { id: choreId }, ) showSuccess({ title: 'Pause queued', message: "You're offline — pause will sync when back online", undoAction: async () => { await commandQueue.cancel(cmdId) }, }) return } showError({ title: 'Failed to pause timer', message: 'Please try again.', }) }, onSettled: () => { setTimerActionLoading(false) }, }) } // Determine if timer is currently running const isTimerRunning = () => { if (!timerData || !timerData.pauseLog) return false return timerData.pauseLog.some(session => session.start && !session.end) } // Calculate total duration from start to now/end (real-time) const calculateTotalDuration = () => { if (!timerData) return 0 const startTime = new Date(timerData.startTime) const endTime = timerData.endTime ? new Date(timerData.endTime) : currentTime return Math.floor((endTime - startTime) / 1000) // in seconds } // Calculate current active duration (including ongoing session) (real-time) const calculateCurrentActiveDuration = () => { if (!timerData || !timerData.pauseLog) return 0 let totalActive = 0 const now = currentTime timerData.pauseLog.forEach(session => { if (session.start && session.end) { // Completed session totalActive += Math.floor( (new Date(session.end) - new Date(session.start)) / 1000, ) } else if (session.start && !session.end) { // Ongoing session - real-time calculation totalActive += Math.floor((now - new Date(session.start)) / 1000) } }) return totalActive } // Calculate idle time (total time minus active time) (real-time) const calculateIdleTime = () => { const totalDuration = calculateTotalDuration() const activeDuration = calculateCurrentActiveDuration() return Math.max(0, totalDuration - activeDuration) } const handleEditSession = () => { startEditingSession() } const handleDeleteSession = sessionIndex => { // For now, just show an alert since we'd need to implement session deletion API showError({ title: 'Delete Session', message: `Session #${sessionIndex + 1} deletion would be implemented here`, }) } if (loading || isCircleMembersLoading) { return } return ( {/* Header */} {loading && ( Loading timer data... )} {!loading && !timerData && ( No timer data found for this chore. )} {!loading && timerData && ( {/* Timer Summary */} {/* Stats Grid */} {/* Active Time */} Active Work {formatDuration(calculateCurrentActiveDuration())} {/* Idle Time */} Break Time {formatDuration(calculateIdleTime())} {/* Total Sessions */} Sessions {timerData.pauseLog?.length || 0} {/* Total Session Time */} Total Time {formatTime(calculateTotalDuration())} {/* Progress Bar */} Work vs Break Distribution {calculateCurrentActiveDuration() > 0 ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active` : 'No active time yet'} {/* Timeline Graph */} Activity Timeline {timerData && timerData.pauseLog && timerData.pauseLog.length > 0 ? ( {/* Timeline visualization */} {(() => { const totalDuration = calculateTotalDuration() const startTime = new Date(timerData.startTime) return timerData.pauseLog.map((session, index) => { const sessionStart = new Date(session.start) const sessionEnd = session.end ? new Date(session.end) : currentTime // Calculate position and width as percentages const startOffset = Math.max( 0, (sessionStart - startTime) / 1000, ) const sessionDuration = Math.max( 0, (sessionEnd - sessionStart) / 1000, ) const leftPercent = (startOffset / Math.max(totalDuration, 1)) * 100 const widthPercent = (sessionDuration / Math.max(totalDuration, 1)) * 100 const isOngoing = !session.end return ( ) }) })()} {/* Legend and time markers */} {/* Legend */} Active Work Break Time {isTimerRunning() && ( Live Session )} {/* Time markers */} Started: {fmt.time(timerData.startTime)} {timerData.endTime && ( Ended: {fmt.time(timerData.endTime)} )} {!timerData.endTime && ( Now: {fmt.time(currentTime)} )} Active:{' '} {calculateCurrentActiveDuration() > 0 ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}%` : '0%'} ) : ( No activity timeline available. Start working to see your activity pattern. )} {/* Session Breakdown */} Session Breakdown {!editingSessions[timerData.id] && ( )} {editingSessions[timerData.id] && ( )} {!editingSessions[timerData.id] ? ( {/* Read-only view */} {timerData.pauseLog && timerData.pauseLog.length > 0 && ( Work Sessions ({timerData.pauseLog.length}) {timerData.pauseLog .sort((a, b) => moment(b.start) - moment(a.start)) .map((pause, pauseIndex) => { const isOngoing = !pause.end const sessionDate = moment(pause.start).format( 'MMM DD', ) const startTime = fmt.time(pause.start) const endTime = pause.end ? fmt.time(pause.end) : null const realTimeDuration = isOngoing ? Math.max( 0, Math.floor( (currentTime - new Date(pause.start)) / 1000, ), ) : pause.duration return ( handleEditSession()} > Edit handleDeleteSession(pauseIndex) } > Delete } > {/* Session Card Content */} {/* Session indicator */} {/* Duration - Main focus */} {formatDuration(realTimeDuration)} {isOngoing && ( Live )} {/* User chip showing who started the session */} {pause.updatedBy && pause.updatedBy !== 0 && (() => { const sessionUser = getMemberById( pause.updatedBy, ) return sessionUser ? ( {sessionUser?.displayName?.charAt( 0, ) || sessionUser?.name?.charAt( 0, ) || } } sx={{ fontSize: '0.7rem' }} > {sessionUser?.displayName || sessionUser?.name || 'Unknown'} ) : null })()} {/* Session details */} Session #{pauseIndex + 1} • {sessionDate} {startTime}{' '} {endTime ? `→ ${endTime}` : '→ ongoing'} { e.stopPropagation() if (showMoreInfoId === pauseIndex) { setShowMoreInfoId(null) } else { setShowMoreInfoId(pauseIndex) } }} > ) })} )} {(!timerData.pauseLog || timerData.pauseLog.length === 0) && ( No work sessions found for this timer. )} ) : ( {/* Editing view */} {/* Session Editor */} Sessions {editingSessions[timerData.id].pauseLog.map( (pause, pauseIndex) => ( Session #{pauseIndex + 1} Start Time updatePauseLogEntry( timerData.id, pauseIndex, 'start', new Date(e.target.value).toISOString(), ) } /> End Time updatePauseLogEntry( timerData.id, pauseIndex, 'end', e.target.value ? new Date(e.target.value).toISOString() : null, ) } /> Leave empty if session is ongoing Duration (Auto-calculated) {formatDuration(pause.duration)} ( {pause.duration}s) ), )} )} )} {/* Floating Timer Control Button */} {!loading && timerData && ( {isTimerRunning() ? ( ) : ( )} )} ) } export default TimerDetails