enhance impersonation context and user profile handling; improve UI interactions in chore history and timer details

This commit is contained in:
Mo Tarbin
2025-09-15 23:52:28 -04:00
parent 36093b9199
commit b71a533024
10 changed files with 634 additions and 102 deletions

View File

@@ -1,7 +1,7 @@
import {
AdminPanelSettings,
DarkModeOutlined,
Email,
GroupAdd,
LightModeOutlined,
Logout,
Person,
@@ -24,8 +24,9 @@ import {
Typography,
useColorScheme,
} from '@mui/joy'
import { useMediaQuery } from '@mui/material'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
import useStickyState from '../hooks/useStickyState'
@@ -38,29 +39,23 @@ const UserProfileAvatar = () => {
const navigate = useNavigate()
const { mode, setMode } = useColorScheme()
const { data: userProfile } = useUserProfile()
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
const {
isImpersonating,
startImpersonation,
stopImpersonation,
canImpersonate,
getEffectiveUser,
} = useImpersonateUser()
const { data: circleMembersData } = useCircleMembers()
const [isModalOpen, setIsModalOpen] = useState(false)
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
const [isAdmin, setIsAdmin] = useState(false)
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
useEffect(() => {
if (userProfile && userProfile?.id) {
const members = circleMembersData?.res || []
const isUserAdmin = members.some(
member =>
member.userId === userProfile?.id &&
(member.role === 'admin' || member.role === 'manager'),
)
setIsAdmin(isUserAdmin)
}
}, [userProfile, circleMembersData])
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg'))
if (!userProfile) return null
const currentUser = impersonatedUser || userProfile
const isImpersonating = !!impersonatedUser
const currentUser = getEffectiveUser(userProfile)
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
const isPlusUser = isPlusAccount(userProfile)
const getSubscriptionStatus = () => {
@@ -310,7 +305,7 @@ const UserProfileAvatar = () => {
{isImpersonating && (
<MenuItem
onClick={() => setImpersonatedUser(null)}
onClick={() => stopImpersonation()}
sx={{
borderRadius: 'var(--joy-radius-sm)',
'&:hover': {
@@ -367,7 +362,7 @@ const UserProfileAvatar = () => {
</MenuItem>
<MenuItem
onClick={() => navigate('/settings/detailed#sidepanel')}
onClick={() => navigate('/settings/circle')}
sx={{
borderRadius: 'var(--joy-radius-sm)',
'&:hover': {
@@ -375,21 +370,49 @@ const UserProfileAvatar = () => {
},
}}
>
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
<Tune />
<ListItemDecorator sx={{ color: 'var(--joy-palette-primary-500)' }}>
<GroupAdd />
</ListItemDecorator>
<ListItemContent>
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
Sidepanel Settings
Invite People
</Typography>
<Typography
level='body-xs'
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
>
Customize layout & cards
Add members to your circle
</Typography>
</ListItemContent>
</MenuItem>
{isLargeScreen && (
<MenuItem
onClick={() => navigate('/settings/detailed#sidepanel')}
sx={{
borderRadius: 'var(--joy-radius-sm)',
'&:hover': {
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
},
}}
>
<ListItemDecorator
sx={{ color: 'var(--joy-palette-neutral-500)' }}
>
<Tune />
</ListItemDecorator>
<ListItemContent>
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
Side Panel Settings
</Typography>
<Typography
level='body-xs'
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
>
Customize layout & cards
</Typography>
</ListItemContent>
</MenuItem>
)}
<MenuItem
onClick={handleThemeToggle}
@@ -445,7 +468,7 @@ const UserProfileAvatar = () => {
</MenuItem>
)}
<MenuItem
{/* <MenuItem
onClick={handleSupportEmail}
sx={{
borderRadius: 'var(--joy-radius-sm)',
@@ -468,7 +491,7 @@ const UserProfileAvatar = () => {
support@donetick.com
</Typography>
</ListItemContent>
</MenuItem>
</MenuItem> */}
<Divider sx={{ my: 1 }} />
@@ -500,7 +523,7 @@ const UserProfileAvatar = () => {
isOpen={isModalOpen}
performers={circleMembersData?.res}
onSelect={user => {
setImpersonatedUser(user)
startImpersonation(user, userProfile)
setIsModalOpen(false)
}}
onClose={() => setIsModalOpen(false)}

View File

@@ -1,15 +1,150 @@
import { createContext, useContext, useState } from 'react'
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
const ImpersonateUserContext = createContext()
export const useImpersonateUser = () => useContext(ImpersonateUserContext)
export const ImpersonateUserProvider = ({ children }) => {
const [impersonatedUser, setImpersonatedUser] = useState(null)
const [impersonationState, setImpersonationState] = useState({
isImpersonating: false,
impersonatedUser: null,
originalUser: null,
})
// Start impersonation
const startImpersonation = useCallback((userToImpersonate, currentUser) => {
console.log('Starting impersonation:', { userToImpersonate, currentUser })
const newState = {
isImpersonating: true,
impersonatedUser: userToImpersonate,
originalUser: currentUser,
}
setImpersonationState(newState)
// Store in localStorage for persistence across page refreshes
localStorage.setItem('impersonation', JSON.stringify(newState))
localStorage.setItem('impersonatedUserId', userToImpersonate.userId)
}, [])
// Stop impersonation
const stopImpersonation = useCallback(() => {
console.log('Stopping impersonation')
setImpersonationState({
isImpersonating: false,
impersonatedUser: null,
originalUser: null,
})
// Remove from localStorage
localStorage.removeItem('impersonation')
localStorage.removeItem('impersonatedUserId')
}, [])
// Get effective user (impersonated user if impersonating, otherwise current user)
const getEffectiveUser = useCallback(
currentUser => {
if (
impersonationState.isImpersonating &&
impersonationState.impersonatedUser
) {
return impersonationState.impersonatedUser
}
return currentUser
},
[impersonationState],
)
// Get impersonation headers for API calls
const getImpersonationHeaders = useCallback(() => {
if (
impersonationState.isImpersonating &&
impersonationState.impersonatedUser
) {
return {
'X-Impersonate-User-ID':
impersonationState.impersonatedUser.id.toString(),
}
}
return {}
}, [impersonationState])
// Check if user can impersonate (admin or manager)
// Note: This is a basic check. The component using this should also check circle membership
const canImpersonate = useCallback((user, circleMembers = []) => {
if (!user?.id) return false
// If circleMembers is provided, check role from there
if (circleMembers.length > 0) {
const member = circleMembers.find(m => m.userId === user.id)
return member?.role === 'admin' || member?.role === 'manager'
}
// Fallback to user.role property (if available)
return user?.role === 'admin' || user?.role === 'manager'
}, [])
// Restore impersonation state from localStorage on mount
useEffect(() => {
const storedImpersonation = localStorage.getItem('impersonation')
if (storedImpersonation) {
try {
const parsed = JSON.parse(storedImpersonation)
if (
parsed.isImpersonating &&
parsed.impersonatedUser &&
parsed.originalUser
) {
setImpersonationState(parsed)
}
} catch (error) {
console.error('Failed to restore impersonation state:', error)
localStorage.removeItem('impersonation')
}
}
}, [])
const value = {
// State
...impersonationState,
// Actions
startImpersonation,
stopImpersonation,
getEffectiveUser,
getImpersonationHeaders,
canImpersonate,
// Computed properties
isImpersonating: impersonationState.isImpersonating,
impersonatedUser: impersonationState.impersonatedUser,
originalUser: impersonationState.originalUser,
// Legacy support
setImpersonatedUser: user => {
if (user) {
// If setting a user, assume we're starting impersonation
// Note: This won't have originalUser, so it's for backward compatibility only
setImpersonationState(prev => ({
isImpersonating: true,
impersonatedUser: user,
originalUser: prev.originalUser,
}))
} else {
stopImpersonation()
}
},
}
return (
<ImpersonateUserContext.Provider
value={{ impersonatedUser, setImpersonatedUser }}
>
<ImpersonateUserContext.Provider value={value}>
{children}
</ImpersonateUserContext.Provider>
)

View File

@@ -137,9 +137,13 @@ export async function Fetch(url, options) {
}
export const HEADERS = () => {
// Import here to avoid circular dependency issues
const impersonateUserId = localStorage.getItem('impersonatedUserId')
return {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('ca_token'),
...(impersonateUserId && { 'X-Impersonate-User-ID': impersonateUserId }),
}
}

View File

@@ -648,7 +648,7 @@ const LoginView = () => {
variant='plain'
size='sm'
onClick={() => {
window.open('https://donetick.com/privacy-policy', '_blank')
window.open('https://donetick.com/privacy', '_blank')
}}
>
Privacy Policy

View File

@@ -6,30 +6,24 @@ import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import UserModal from '../Modals/Inputs/UserModal'
const UserSwitcher = () => {
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
const [isAdmin, setIsAdmin] = useState(false)
const {
impersonatedUser,
isImpersonating,
startImpersonation,
stopImpersonation,
canImpersonate
} = useImpersonateUser()
const { data: userProfile } = useUserProfile()
const [isModalOpen, setIsModalOpen] = useState(false)
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
useEffect(() => {
if (userProfile && userProfile?.id) {
const members = circleMembersData?.res || []
const isUserAdmin = members.some(
member =>
member.userId === userProfile?.id &&
(member.role === 'admin' || member.role === 'manager'),
)
setIsAdmin(isUserAdmin)
}
}, [userProfile, circleMembersData])
// Check if current user can impersonate
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
if (!isAdmin) {
return null
} else if (isCircleMembersLoading || impersonatedUser === null) {
} else if (isCircleMembersLoading || !isImpersonating) {
return (
<Sheet
variant='plain'
@@ -81,7 +75,7 @@ const UserSwitcher = () => {
isOpen={isModalOpen}
performers={circleMembersData?.res}
onSelect={user => {
setImpersonatedUser(user)
startImpersonation(user, userProfile)
setIsModalOpen(false)
}}
onClose={() => setIsModalOpen(false)}
@@ -167,7 +161,7 @@ const UserSwitcher = () => {
size='sm'
sx={{ ml: 0.5 }}
onClick={() => {
setImpersonatedUser(null)
stopImpersonation()
}}
>
Cancel
@@ -180,7 +174,7 @@ const UserSwitcher = () => {
isOpen={isModalOpen}
performers={circleMembersData?.res}
onSelect={user => {
setImpersonatedUser(user)
startImpersonation(user, userProfile)
setIsModalOpen(false)
}}
onClose={() => {

View File

@@ -22,6 +22,7 @@ import moment from 'moment'
import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { LoadingScreen } from '../../components/animations'
import useConfirmationModal from '../../hooks/useConfirmationModal'
import { ChoreHistoryStatus } from '../../utils/Chores'
import {
DeleteChoreHistory,
@@ -29,6 +30,7 @@ import {
GetChoreHistory,
UpdateChoreHistory,
} from '../../utils/Fetcher'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import EditHistoryModal from '../Modals/EditHistoryModal'
import HistoryCard from './HistoryCard'
@@ -42,6 +44,31 @@ const ChoreHistory = () => {
const { choreId } = useParams()
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [editHistory, setEditHistory] = useState({})
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
const handleDelete = historyEntry => {
showConfirmation(
`Are you sure you want to delete this history record?`,
'Delete History Record',
() => {
DeleteChoreHistory(choreId, historyEntry.id).then(() => {
const newHistory = choreHistory.filter(
record => record.id !== historyEntry.id,
)
setChoresHistory(newHistory)
updateHistoryInfo(newHistory, userHistory, performers)
})
},
'Delete',
'Cancel',
'danger',
)
}
const handleEdit = historyEntry => {
setIsEditModalOpen(true)
setEditHistory(historyEntry)
}
useEffect(() => {
setIsLoading(true) // Start loading
@@ -281,10 +308,9 @@ const ChoreHistory = () => {
<List sx={{ p: 0 }}>
{choreHistory.map((historyEntry, index) => (
<HistoryCard
onClick={() => {
setIsEditModalOpen(true)
setEditHistory(historyEntry)
}}
onClick={() => handleEdit(historyEntry)}
onEditClick={handleEdit}
onDeleteClick={handleDelete}
historyEntry={historyEntry}
performers={performers}
allHistory={choreHistory}
@@ -334,6 +360,7 @@ const ChoreHistory = () => {
}}
historyRecord={editHistory}
/>
<ConfirmationModal config={confirmModalConfig} />
</Container>
)
}

View File

@@ -3,6 +3,8 @@ import {
CalendarMonth,
Check,
CheckCircle,
Delete,
Edit,
EventNote,
HourglassEmpty,
Person,
@@ -16,12 +18,14 @@ import {
Box,
Chip,
Grid,
IconButton,
ListDivider,
ListItem,
ListItemContent,
Typography,
} from '@mui/joy'
import moment from 'moment'
import React, { useEffect, useRef, useState } from 'react'
import { TASK_COLOR } from '../../utils/Colors.jsx'
const getCompletedChip = historyEntry => {
@@ -100,10 +104,22 @@ const HistoryCard = ({
historyEntry,
index,
onClick,
onEditClick,
onDeleteClick,
}) => {
const performer = performers.find(p => p.userId === historyEntry.completedBy)
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
// Swipe functionality state
const [swipeTranslateX, setSwipeTranslateX] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const [isSwipeRevealed, setIsSwipeRevealed] = useState(false)
const [hoverTimer, setHoverTimer] = useState(null)
const swipeThreshold = 80
const maxSwipeDistance = 200
const dragStartX = useRef(0)
const cardRef = useRef(null)
const formatTimeDifference = (startDate, endDate) => {
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
let timeValue = diffInMinutes
@@ -150,23 +166,265 @@ const HistoryCard = ({
)
}
// Swipe gesture handlers
const handleTouchStart = e => {
dragStartX.current = e.touches[0].clientX
setIsDragging(true)
}
const handleTouchMove = e => {
if (!isDragging) return
const currentX = e.touches[0].clientX
const deltaX = currentX - dragStartX.current
if (isSwipeRevealed) {
if (deltaX > 0) {
const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
setSwipeTranslateX(clampedDelta)
}
} else {
if (deltaX < 0) {
const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
setSwipeTranslateX(clampedDelta)
}
}
}
const handleTouchEnd = () => {
if (!isDragging) return
setIsDragging(false)
if (isSwipeRevealed) {
if (swipeTranslateX > -swipeThreshold) {
setSwipeTranslateX(0)
setIsSwipeRevealed(false)
} else {
setSwipeTranslateX(-maxSwipeDistance)
}
} else {
if (Math.abs(swipeTranslateX) > swipeThreshold) {
setSwipeTranslateX(-maxSwipeDistance)
setIsSwipeRevealed(true)
} else {
setSwipeTranslateX(0)
setIsSwipeRevealed(false)
}
}
}
const handleMouseDown = e => {
dragStartX.current = e.clientX
setIsDragging(true)
}
const handleMouseMove = e => {
if (!isDragging) return
const currentX = e.clientX
const deltaX = currentX - dragStartX.current
if (isSwipeRevealed) {
if (deltaX > 0) {
const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
setSwipeTranslateX(clampedDelta)
}
} else {
if (deltaX < 0) {
const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
setSwipeTranslateX(clampedDelta)
}
}
}
const handleMouseUp = () => {
if (!isDragging) return
setIsDragging(false)
if (isSwipeRevealed) {
if (swipeTranslateX > -swipeThreshold) {
setSwipeTranslateX(0)
setIsSwipeRevealed(false)
} else {
setSwipeTranslateX(-maxSwipeDistance)
}
} else {
if (Math.abs(swipeTranslateX) > swipeThreshold) {
setSwipeTranslateX(-maxSwipeDistance)
setIsSwipeRevealed(true)
} else {
setSwipeTranslateX(0)
setIsSwipeRevealed(false)
}
}
}
const resetSwipe = () => {
setSwipeTranslateX(0)
setIsSwipeRevealed(false)
}
// Hover functionality for desktop - only trigger from drag area
const handleMouseEnter = () => {
if (isSwipeRevealed) return
const timer = setTimeout(() => {
setSwipeTranslateX(-maxSwipeDistance)
setIsSwipeRevealed(true)
setHoverTimer(null)
}, 800) // Shorter delay for drag area
setHoverTimer(timer)
}
const handleMouseLeave = () => {
if (hoverTimer) {
clearTimeout(hoverTimer)
setHoverTimer(null)
}
// Only add hide timer if we're leaving the drag area and actions are NOT revealed
// If actions are revealed, let the action area handle the hiding
if (!isSwipeRevealed) {
// Actions are not revealed, so we can safely hide after delay
const hideTimer = setTimeout(() => {
resetSwipe()
}, 300)
setHoverTimer(hideTimer)
}
}
const handleActionAreaMouseEnter = () => {
// Clear any pending timer when entering action area
if (hoverTimer) {
clearTimeout(hoverTimer)
setHoverTimer(null)
}
}
const handleActionAreaMouseLeave = () => {
// Hide immediately when leaving action area
if (isSwipeRevealed) {
resetSwipe()
}
}
// Clean up timer on unmount
useEffect(() => {
return () => {
if (hoverTimer) {
clearTimeout(hoverTimer)
}
}
}, [hoverTimer])
return (
<>
<ListItem
onClick={onClick}
<Box
sx={{
cursor: onClick ? 'pointer' : 'default',
py: 1.5,
px: 2,
'&:hover': onClick
? {
backgroundColor: 'background.level1',
}
: {},
borderRadius: 'sm',
transition: 'background-color 0.2s',
position: 'relative',
overflow: 'hidden',
}}
onMouseLeave={() => {
// Only clear timers, don't auto-hide
if (hoverTimer) {
clearTimeout(hoverTimer)
setHoverTimer(null)
}
}}
>
{/* Action buttons underneath (revealed on swipe) */}
{(onEditClick || onDeleteClick) && (
<Box
sx={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: maxSwipeDistance,
display: 'flex',
alignItems: 'center',
boxShadow: 'inset 2px 0 4px rgba(0,0,0,0.06)',
zIndex: 0,
}}
onMouseEnter={handleActionAreaMouseEnter}
onMouseLeave={handleActionAreaMouseLeave}
>
{onEditClick && (
<IconButton
variant='soft'
color='neutral'
size='sm'
onClick={e => {
e.stopPropagation()
resetSwipe()
onEditClick(historyEntry)
}}
sx={{
width: 40,
height: 40,
mx: 1,
}}
>
<Edit sx={{ fontSize: 16 }} />
</IconButton>
)}
{onDeleteClick && (
<IconButton
variant='soft'
color='danger'
size='sm'
onClick={e => {
e.stopPropagation()
resetSwipe()
onDeleteClick(historyEntry)
}}
sx={{
width: 40,
height: 40,
mx: 1,
}}
>
<Delete sx={{ fontSize: 16 }} />
</IconButton>
)}
</Box>
)}
{/* Main card content */}
<ListItem
ref={cardRef}
onClick={() => {
if (isSwipeRevealed) {
resetSwipe()
return
}
if (onClick) onClick()
}}
sx={{
cursor: onClick ? 'pointer' : 'default',
py: 1.5,
px: 2,
position: 'relative',
bgcolor: 'background.surface',
transform: `translateX(${swipeTranslateX}px)`,
transition: isDragging ? 'none' : 'transform 0.3s ease-out',
zIndex: 1,
width: '100%',
'&:hover': onClick
? {
bgcolor: isSwipeRevealed
? 'background.surface'
: 'background.level1',
}
: {},
borderRadius: 'sm',
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<ListItemContent>
<Grid container spacing={1} alignItems='center'>
{/* First Row/Column: Status and Time Info */}
@@ -303,6 +561,56 @@ const HistoryCard = ({
</Grid>
</Grid>
</ListItemContent>
{/* Right drag area - only triggers reveal on hover */}
{(onEditClick || onDeleteClick) && (
<Box
sx={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: '20px',
cursor: 'grab',
zIndex: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: isSwipeRevealed ? 0 : 0.3, // Hide when action area is revealed
transition: 'opacity 0.2s ease',
pointerEvents: isSwipeRevealed ? 'none' : 'auto', // Disable pointer events when revealed
'&:hover': {
opacity: isSwipeRevealed ? 0 : 0.7,
},
'&:active': {
cursor: 'grabbing',
},
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Drag indicator dots */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 0.25,
}}
>
{[...Array(3)].map((_, i) => (
<Box
key={i}
sx={{
width: 3,
height: 3,
borderRadius: '50%',
backgroundColor: 'text.tertiary',
}}
/>
))}
</Box>
</Box>
)}
</ListItem>
{/* Compact Divider with Time Difference */}
@@ -330,6 +638,7 @@ const HistoryCard = ({
</Typography>
</ListDivider>
)}
</Box>
</>
)
}

View File

@@ -232,7 +232,7 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
onMouseLeave={handleActionAreaMouseLeave}
>
<IconButton
variant='plain'
variant='soft'
color='neutral'
size='sm'
onClick={e => {
@@ -244,18 +244,13 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
width: 40,
height: 40,
mx: 1,
bgcolor: 'primary.100',
color: 'primary.600',
'&:hover': {
bgcolor: 'primary.200',
},
}}
>
<EditIcon sx={{ fontSize: 16 }} />
</IconButton>
<IconButton
variant='plain'
variant='soft'
color='danger'
size='sm'
onClick={e => {
@@ -267,11 +262,6 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
width: 40,
height: 40,
mx: 1,
bgcolor: 'danger.100',
color: 'danger.600',
'&:hover': {
bgcolor: 'danger.200',
},
}}
>
<DeleteIcon sx={{ fontSize: 16 }} />

View File

@@ -25,7 +25,12 @@ function EditHistoryModal({ config, historyRecord }) {
const [notes, setNotes] = useState(historyRecord.notes)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
return (
<ResponsiveModal open={config?.isOpen} onClose={config?.onClose}>
<ResponsiveModal
open={config?.isOpen}
onClose={config?.onClose}
size='lg'
// fullWidth={true}
>
<Typography level='h4' mb={1}>
Edit History
</Typography>
@@ -82,19 +87,9 @@ function EditHistoryModal({ config, historyRecord }) {
>
Save
</Button>
<Button size='' onClick={config.onClose} variant='outlined'>
<Button fullWidth size='lg' onClick={config.onClose} variant='outlined'>
Cancel
</Button>
<Button
size='lg'
onClick={() => {
setIsDeleteModalOpen(true)
}}
variant='outlined'
color='danger'
>
Delete
</Button>
</Box>
<ConfirmationModal
config={{

View File

@@ -5,10 +5,12 @@ import {
Delete,
Edit,
PauseCircle,
Person,
PlayArrow,
} from '@mui/icons-material'
import {
Alert,
Avatar,
Box,
Button,
Card,
@@ -25,6 +27,7 @@ import {
import moment from 'moment'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useCircleMembers } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import {
GetChoreTimer,
@@ -32,6 +35,7 @@ import {
StartChore,
UpdateTimeSession,
} from '../../utils/Fetcher'
import { resolvePhotoURL } from '../../utils/Helpers'
const TimerDetails = () => {
const { choreId } = useParams()
@@ -50,6 +54,17 @@ const TimerDetails = () => {
const [isDragging, setIsDragging] = useState(false)
const [isTouchDevice, setIsTouchDevice] = useState(false)
// Fetch circle members data
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
const members = circleMembersData?.res || []
// Helper function to find member by user ID
const getMemberById = userId => {
return members?.find(member => member.userId === userId)
}
// Detect if device supports touch
useEffect(() => {
const checkTouchDevice = () => {
@@ -1230,16 +1245,56 @@ const TimerDetails = () => {
>
{formatDuration(realTimeDuration)}
</Typography>
{isOngoing && (
<Chip
size='sm'
color='success'
variant='soft'
sx={{ fontSize: '0.7rem' }}
>
Live
</Chip>
)}
<Box
sx={{ display: 'flex', gap: 0.5, mt: 0.5 }}
>
{isOngoing && (
<Chip
size='sm'
color='success'
variant='soft'
sx={{ fontSize: '0.7rem' }}
>
Live
</Chip>
)}
{/* User chip showing who started the session */}
{pause.updatedBy &&
pause.updatedBy !== 0 &&
(() => {
const sessionUser = getMemberById(
pause.updatedBy,
)
return sessionUser ? (
<Chip
size='sm'
variant='outlined'
color='neutral'
startDecorator={
<Avatar
size='sm'
src={resolvePhotoURL(
sessionUser?.image,
)}
sx={{ width: 16, height: 16 }}
>
{sessionUser?.displayName?.charAt(
0,
) ||
sessionUser?.name?.charAt(
0,
) || <Person />}
</Avatar>
}
sx={{ fontSize: '0.7rem' }}
>
{sessionUser?.displayName ||
sessionUser?.name ||
'Unknown'}
</Chip>
) : null
})()}
</Box>
</Box>
{/* Session details */}