feat: add TimerEditModal component for managing timer sessions with real-time updates and editing capabilities
This commit is contained in:
870
src/views/Modals/Inputs/TimerEditModal.jsx
Normal file
870
src/views/Modals/Inputs/TimerEditModal.jsx
Normal file
@@ -0,0 +1,870 @@
|
||||
import { Add, Delete, Edit } from '@mui/icons-material'
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FadeModal from '../../../components/common/FadeModal'
|
||||
import { useNotification } from '../../../service/NotificationProvider'
|
||||
import {
|
||||
DeleteTimeSession,
|
||||
GetChoreTimer,
|
||||
UpdateTimeSession,
|
||||
} from '../../../utils/Fetcher'
|
||||
import ConfirmationModal from './ConfirmationModal'
|
||||
|
||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const [timerData, setTimerData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editingSessions, setEditingSessions] = useState({})
|
||||
const [confirmDeleteConfig, setConfirmDeleteConfig] = useState({})
|
||||
const [currentTime, setCurrentTime] = useState(new Date())
|
||||
const { showError, showSuccess } = useNotification()
|
||||
|
||||
// Fetch timer data when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && choreId) {
|
||||
fetchTimerData()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, choreId])
|
||||
|
||||
// Real-time update interval for active timers
|
||||
useEffect(() => {
|
||||
let interval
|
||||
if (isOpen && timerData && !timerData.endTime) {
|
||||
// Update every second if timer is active
|
||||
interval = setInterval(() => {
|
||||
setCurrentTime(new Date())
|
||||
}, 1000)
|
||||
}
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [isOpen, timerData])
|
||||
|
||||
const fetchTimerData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await GetChoreTimer(choreId)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setTimerData(data.res) // data.res is the timer session object
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to fetch timer data',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error fetching timer data',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
const response = await UpdateTimeSession(choreId, sessionId, updateData)
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Session updated',
|
||||
message: 'Timer session has been updated successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
cancelEditingSession(sessionId)
|
||||
onTimerUpdate?.()
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to update session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error updating session',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSession = async sessionId => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await DeleteTimeSession(choreId, sessionId)
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Session deleted',
|
||||
message: 'Timer session has been deleted successfully.',
|
||||
})
|
||||
await fetchTimerData()
|
||||
onTimerUpdate?.()
|
||||
} else {
|
||||
showError({
|
||||
title: 'Failed to delete session',
|
||||
message: 'Please try again.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Error deleting session',
|
||||
message: error.message,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteSession = sessionId => {
|
||||
setConfirmDeleteConfig({
|
||||
isOpen: true,
|
||||
title: 'Delete Timer Session',
|
||||
message: 'Are you sure you want to delete this timer session?',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
color: 'danger',
|
||||
onClose: isConfirmed => {
|
||||
if (isConfirmed) {
|
||||
deleteSession(sessionId)
|
||||
}
|
||||
setConfirmDeleteConfig({})
|
||||
setEditingSessions({})
|
||||
onClose?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setEditingSessions({})
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FadeModal open={isOpen} onClose={onClose} size='lg' fullWidth={true}>
|
||||
<Typography level='h4'>Timer Details</Typography>
|
||||
|
||||
{loading && (
|
||||
<Alert color='neutral' sx={{ mb: 2 }}>
|
||||
Loading timer data...
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!loading && !timerData && (
|
||||
<Alert color='warning' sx={{ mb: 2 }}>
|
||||
No timer data found for this chore.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!loading && timerData && (
|
||||
<Box sx={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
||||
{/* Timer Summary */}
|
||||
<Card
|
||||
variant='plain'
|
||||
sx={{
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{/* Header with timeline */}
|
||||
|
||||
{/* Stats Grid */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* Active Time */}
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'success.500',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'success.600',
|
||||
fontWeight: 'bold',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateCurrentActiveDuration())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Active Work
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Idle Time */}
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'warning.500',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'warning.600',
|
||||
fontWeight: 'bold',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(calculateIdleTime())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Break Time
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Total Sessions */}
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.500',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'primary.600',
|
||||
fontWeight: 'bold',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{timerData.pauseLog?.length || 0}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Work Sessions
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Total Session Time */}
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
p: 2,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.500',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
color: 'neutral.700',
|
||||
fontWeight: 'bold',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatTime(calculateTotalDuration())}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Total Time
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.secondary', fontWeight: 'medium' }}
|
||||
>
|
||||
Work vs Break Distribution
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary' }}>
|
||||
{calculateCurrentActiveDuration() > 0
|
||||
? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active`
|
||||
: 'No active time yet'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
height: 6,
|
||||
backgroundColor: 'neutral.200',
|
||||
borderRadius: 'sm',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
width: `${Math.round((calculateCurrentActiveDuration() / Math.max(calculateTotalDuration(), 1)) * 100)}%`,
|
||||
backgroundColor: 'success.400',
|
||||
borderRadius: 'sm',
|
||||
transition: 'width 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Time Session */}
|
||||
<Box>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Session Breakdown
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
{!editingSessions[timerData.id] ? (
|
||||
<Box>
|
||||
{/* Read-only view */}
|
||||
{/* Sessions */}
|
||||
{timerData.pauseLog && timerData.pauseLog.length > 0 && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 'bold', mb: 2 }}
|
||||
>
|
||||
Work Sessions ({timerData.pauseLog.length})
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{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 = moment(pause.start).format(
|
||||
'HH:mm',
|
||||
)
|
||||
const endTime = pause.end
|
||||
? moment(pause.end).format('HH:mm')
|
||||
: null
|
||||
|
||||
const realTimeDuration = isOngoing
|
||||
? Math.max(
|
||||
0,
|
||||
Math.floor(
|
||||
(currentTime - new Date(pause.start)) /
|
||||
1000,
|
||||
),
|
||||
)
|
||||
: pause.duration
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={pauseIndex}
|
||||
variant='outlined'
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
minHeight: 'auto',
|
||||
|
||||
borderColor: isOngoing
|
||||
? 'success.300'
|
||||
: 'divider',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Session indicator */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: isOngoing
|
||||
? 'success.500'
|
||||
: 'neutral.400',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Duration - Main focus */}
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Typography
|
||||
level='h4'
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
color: isOngoing
|
||||
? 'success.600'
|
||||
: 'text.primary',
|
||||
lineHeight: 1,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{formatDuration(realTimeDuration)}
|
||||
</Typography>
|
||||
{isOngoing && (
|
||||
<Chip
|
||||
size='sm'
|
||||
color='success'
|
||||
variant='soft'
|
||||
sx={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
Live
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Session details */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
// this align to the right side of the card
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: 'medium',
|
||||
color: 'text.secondary',
|
||||
mb: 0.3,
|
||||
}}
|
||||
>
|
||||
Session #{pauseIndex + 1} • {sessionDate}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'text.tertiary',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{startTime}{' '}
|
||||
{endTime ? `→ ${endTime}` : '→ ongoing'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
{/* Editing view */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{/* Session Editor */}
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Sessions
|
||||
</Typography>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
startDecorator={<Add />}
|
||||
onClick={() => addPauseLogEntry(timerData.id)}
|
||||
>
|
||||
Add Session
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{editingSessions[timerData.id].pauseLog.map(
|
||||
(pause, pauseIndex) => (
|
||||
<Card
|
||||
key={pauseIndex}
|
||||
variant='soft'
|
||||
sx={{ mb: 2, p: 2 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Session #{pauseIndex + 1}
|
||||
</Typography>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() =>
|
||||
deletePauseLogEntry(
|
||||
timerData.id,
|
||||
pauseIndex,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Delete />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<FormControl size='sm'>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Start Time
|
||||
</Typography>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
value={moment(pause.start).format(
|
||||
'YYYY-MM-DDTHH:mm:ss',
|
||||
)}
|
||||
onChange={e =>
|
||||
updatePauseLogEntry(
|
||||
timerData.id,
|
||||
pauseIndex,
|
||||
'start',
|
||||
new Date(e.target.value).toISOString(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size='sm'>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
End Time
|
||||
</Typography>
|
||||
<Input
|
||||
type='datetime-local'
|
||||
value={
|
||||
pause.end
|
||||
? moment(pause.end).format(
|
||||
'YYYY-MM-DDTHH:mm:ss',
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={e =>
|
||||
updatePauseLogEntry(
|
||||
timerData.id,
|
||||
pauseIndex,
|
||||
'end',
|
||||
e.target.value
|
||||
? new Date(
|
||||
e.target.value,
|
||||
).toISOString()
|
||||
: null,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Leave empty if session is ongoing
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ fontWeight: 'bold', mb: 0.5 }}
|
||||
>
|
||||
Duration (Auto-calculated)
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
p: 1,
|
||||
bgcolor: 'background.surface',
|
||||
borderRadius: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{formatDuration(pause.duration)} (
|
||||
{pause.duration}s)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{!timerData && (
|
||||
<Alert color='neutral' sx={{ mt: 2 }}>
|
||||
No timer session found for this chore.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant='outlined' onClick={handleClose} color='neutral'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{/* Action buttons on the right */}
|
||||
{!loading && timerData && !editingSessions[timerData.id] && (
|
||||
<>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => confirmDeleteSession(timerData.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startDecorator={<Edit />}
|
||||
onClick={() => startEditingSession()}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Save button when editing */}
|
||||
{!loading && timerData && editingSessions[timerData.id] && (
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={() => saveSession(timerData.id)}
|
||||
loading={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</FadeModal>
|
||||
|
||||
<ConfirmationModal config={confirmDeleteConfig} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimerEditModal
|
||||
Reference in New Issue
Block a user