enhance impersonation context and user profile handling; improve UI interactions in chore history and timer details
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AdminPanelSettings,
|
AdminPanelSettings,
|
||||||
DarkModeOutlined,
|
DarkModeOutlined,
|
||||||
Email,
|
GroupAdd,
|
||||||
LightModeOutlined,
|
LightModeOutlined,
|
||||||
Logout,
|
Logout,
|
||||||
Person,
|
Person,
|
||||||
@@ -24,8 +24,9 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
|
import { useMediaQuery } from '@mui/material'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
|
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
|
||||||
import useStickyState from '../hooks/useStickyState'
|
import useStickyState from '../hooks/useStickyState'
|
||||||
@@ -38,29 +39,23 @@ const UserProfileAvatar = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { mode, setMode } = useColorScheme()
|
const { mode, setMode } = useColorScheme()
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
const {
|
||||||
|
isImpersonating,
|
||||||
|
startImpersonation,
|
||||||
|
stopImpersonation,
|
||||||
|
canImpersonate,
|
||||||
|
getEffectiveUser,
|
||||||
|
} = useImpersonateUser()
|
||||||
const { data: circleMembersData } = useCircleMembers()
|
const { data: circleMembersData } = useCircleMembers()
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
|
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
|
||||||
const [isAdmin, setIsAdmin] = useState(false)
|
|
||||||
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
||||||
|
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg'))
|
||||||
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])
|
|
||||||
|
|
||||||
if (!userProfile) return null
|
if (!userProfile) return null
|
||||||
|
|
||||||
const currentUser = impersonatedUser || userProfile
|
const currentUser = getEffectiveUser(userProfile)
|
||||||
const isImpersonating = !!impersonatedUser
|
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
|
||||||
const isPlusUser = isPlusAccount(userProfile)
|
const isPlusUser = isPlusAccount(userProfile)
|
||||||
|
|
||||||
const getSubscriptionStatus = () => {
|
const getSubscriptionStatus = () => {
|
||||||
@@ -310,7 +305,7 @@ const UserProfileAvatar = () => {
|
|||||||
|
|
||||||
{isImpersonating && (
|
{isImpersonating && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setImpersonatedUser(null)}
|
onClick={() => stopImpersonation()}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
borderRadius: 'var(--joy-radius-sm)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -367,7 +362,7 @@ const UserProfileAvatar = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => navigate('/settings/detailed#sidepanel')}
|
onClick={() => navigate('/settings/circle')}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
borderRadius: 'var(--joy-radius-sm)',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -375,21 +370,49 @@ const UserProfileAvatar = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-primary-500)' }}>
|
||||||
<Tune />
|
<GroupAdd />
|
||||||
</ListItemDecorator>
|
</ListItemDecorator>
|
||||||
<ListItemContent>
|
<ListItemContent>
|
||||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||||
Sidepanel Settings
|
Invite People
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-xs'
|
level='body-xs'
|
||||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||||
>
|
>
|
||||||
Customize layout & cards
|
Add members to your circle
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</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
|
<MenuItem
|
||||||
onClick={handleThemeToggle}
|
onClick={handleThemeToggle}
|
||||||
@@ -445,7 +468,7 @@ const UserProfileAvatar = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MenuItem
|
{/* <MenuItem
|
||||||
onClick={handleSupportEmail}
|
onClick={handleSupportEmail}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 'var(--joy-radius-sm)',
|
borderRadius: 'var(--joy-radius-sm)',
|
||||||
@@ -468,7 +491,7 @@ const UserProfileAvatar = () => {
|
|||||||
support@donetick.com
|
support@donetick.com
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
|
|
||||||
<Divider sx={{ my: 1 }} />
|
<Divider sx={{ my: 1 }} />
|
||||||
|
|
||||||
@@ -500,7 +523,7 @@ const UserProfileAvatar = () => {
|
|||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
performers={circleMembersData?.res}
|
performers={circleMembersData?.res}
|
||||||
onSelect={user => {
|
onSelect={user => {
|
||||||
setImpersonatedUser(user)
|
startImpersonation(user, userProfile)
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
}}
|
}}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
|||||||
@@ -1,15 +1,150 @@
|
|||||||
import { createContext, useContext, useState } from 'react'
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
const ImpersonateUserContext = createContext()
|
const ImpersonateUserContext = createContext()
|
||||||
|
|
||||||
export const useImpersonateUser = () => useContext(ImpersonateUserContext)
|
export const useImpersonateUser = () => useContext(ImpersonateUserContext)
|
||||||
|
|
||||||
export const ImpersonateUserProvider = ({ children }) => {
|
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 (
|
return (
|
||||||
<ImpersonateUserContext.Provider
|
<ImpersonateUserContext.Provider value={value}>
|
||||||
value={{ impersonatedUser, setImpersonatedUser }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ImpersonateUserContext.Provider>
|
</ImpersonateUserContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -137,9 +137,13 @@ export async function Fetch(url, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HEADERS = () => {
|
export const HEADERS = () => {
|
||||||
|
// Import here to avoid circular dependency issues
|
||||||
|
const impersonateUserId = localStorage.getItem('impersonatedUserId')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: 'Bearer ' + localStorage.getItem('ca_token'),
|
Authorization: 'Bearer ' + localStorage.getItem('ca_token'),
|
||||||
|
...(impersonateUserId && { 'X-Impersonate-User-ID': impersonateUserId }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ const LoginView = () => {
|
|||||||
variant='plain'
|
variant='plain'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open('https://donetick.com/privacy-policy', '_blank')
|
window.open('https://donetick.com/privacy', '_blank')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
|
|||||||
@@ -6,30 +6,24 @@ import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
|||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import UserModal from '../Modals/Inputs/UserModal'
|
import UserModal from '../Modals/Inputs/UserModal'
|
||||||
const UserSwitcher = () => {
|
const UserSwitcher = () => {
|
||||||
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
const {
|
||||||
const [isAdmin, setIsAdmin] = useState(false)
|
impersonatedUser,
|
||||||
|
isImpersonating,
|
||||||
|
startImpersonation,
|
||||||
|
stopImpersonation,
|
||||||
|
canImpersonate
|
||||||
|
} = useImpersonateUser()
|
||||||
const { data: userProfile } = useUserProfile()
|
const { data: userProfile } = useUserProfile()
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||||
useCircleMembers()
|
useCircleMembers()
|
||||||
|
|
||||||
useEffect(() => {
|
// Check if current user can impersonate
|
||||||
if (userProfile && userProfile?.id) {
|
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
|
||||||
const members = circleMembersData?.res || []
|
|
||||||
const isUserAdmin = members.some(
|
|
||||||
member =>
|
|
||||||
member.userId === userProfile?.id &&
|
|
||||||
(member.role === 'admin' || member.role === 'manager'),
|
|
||||||
)
|
|
||||||
|
|
||||||
setIsAdmin(isUserAdmin)
|
|
||||||
}
|
|
||||||
}, [userProfile, circleMembersData])
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return null
|
return null
|
||||||
} else if (isCircleMembersLoading || impersonatedUser === null) {
|
} else if (isCircleMembersLoading || !isImpersonating) {
|
||||||
return (
|
return (
|
||||||
<Sheet
|
<Sheet
|
||||||
variant='plain'
|
variant='plain'
|
||||||
@@ -81,7 +75,7 @@ const UserSwitcher = () => {
|
|||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
performers={circleMembersData?.res}
|
performers={circleMembersData?.res}
|
||||||
onSelect={user => {
|
onSelect={user => {
|
||||||
setImpersonatedUser(user)
|
startImpersonation(user, userProfile)
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
}}
|
}}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
@@ -167,7 +161,7 @@ const UserSwitcher = () => {
|
|||||||
size='sm'
|
size='sm'
|
||||||
sx={{ ml: 0.5 }}
|
sx={{ ml: 0.5 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImpersonatedUser(null)
|
stopImpersonation()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -180,7 +174,7 @@ const UserSwitcher = () => {
|
|||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
performers={circleMembersData?.res}
|
performers={circleMembersData?.res}
|
||||||
onSelect={user => {
|
onSelect={user => {
|
||||||
setImpersonatedUser(user)
|
startImpersonation(user, userProfile)
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import moment from 'moment'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { LoadingScreen } from '../../components/animations'
|
import { LoadingScreen } from '../../components/animations'
|
||||||
|
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||||
import { ChoreHistoryStatus } from '../../utils/Chores'
|
import { ChoreHistoryStatus } from '../../utils/Chores'
|
||||||
import {
|
import {
|
||||||
DeleteChoreHistory,
|
DeleteChoreHistory,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
GetChoreHistory,
|
GetChoreHistory,
|
||||||
UpdateChoreHistory,
|
UpdateChoreHistory,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
import EditHistoryModal from '../Modals/EditHistoryModal'
|
import EditHistoryModal from '../Modals/EditHistoryModal'
|
||||||
import HistoryCard from './HistoryCard'
|
import HistoryCard from './HistoryCard'
|
||||||
|
|
||||||
@@ -42,6 +44,31 @@ const ChoreHistory = () => {
|
|||||||
const { choreId } = useParams()
|
const { choreId } = useParams()
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||||
const [editHistory, setEditHistory] = useState({})
|
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(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true) // Start loading
|
setIsLoading(true) // Start loading
|
||||||
@@ -281,10 +308,9 @@ const ChoreHistory = () => {
|
|||||||
<List sx={{ p: 0 }}>
|
<List sx={{ p: 0 }}>
|
||||||
{choreHistory.map((historyEntry, index) => (
|
{choreHistory.map((historyEntry, index) => (
|
||||||
<HistoryCard
|
<HistoryCard
|
||||||
onClick={() => {
|
onClick={() => handleEdit(historyEntry)}
|
||||||
setIsEditModalOpen(true)
|
onEditClick={handleEdit}
|
||||||
setEditHistory(historyEntry)
|
onDeleteClick={handleDelete}
|
||||||
}}
|
|
||||||
historyEntry={historyEntry}
|
historyEntry={historyEntry}
|
||||||
performers={performers}
|
performers={performers}
|
||||||
allHistory={choreHistory}
|
allHistory={choreHistory}
|
||||||
@@ -334,6 +360,7 @@ const ChoreHistory = () => {
|
|||||||
}}
|
}}
|
||||||
historyRecord={editHistory}
|
historyRecord={editHistory}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
CalendarMonth,
|
CalendarMonth,
|
||||||
Check,
|
Check,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
Delete,
|
||||||
|
Edit,
|
||||||
EventNote,
|
EventNote,
|
||||||
HourglassEmpty,
|
HourglassEmpty,
|
||||||
Person,
|
Person,
|
||||||
@@ -16,12 +18,14 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
Grid,
|
Grid,
|
||||||
|
IconButton,
|
||||||
ListDivider,
|
ListDivider,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemContent,
|
ListItemContent,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||||
|
|
||||||
const getCompletedChip = historyEntry => {
|
const getCompletedChip = historyEntry => {
|
||||||
@@ -100,10 +104,22 @@ const HistoryCard = ({
|
|||||||
historyEntry,
|
historyEntry,
|
||||||
index,
|
index,
|
||||||
onClick,
|
onClick,
|
||||||
|
onEditClick,
|
||||||
|
onDeleteClick,
|
||||||
}) => {
|
}) => {
|
||||||
const performer = performers.find(p => p.userId === historyEntry.completedBy)
|
const performer = performers.find(p => p.userId === historyEntry.completedBy)
|
||||||
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
|
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 formatTimeDifference = (startDate, endDate) => {
|
||||||
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
|
||||||
let timeValue = diffInMinutes
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<Box
|
||||||
onClick={onClick}
|
|
||||||
sx={{
|
sx={{
|
||||||
cursor: onClick ? 'pointer' : 'default',
|
position: 'relative',
|
||||||
py: 1.5,
|
overflow: 'hidden',
|
||||||
px: 2,
|
}}
|
||||||
'&:hover': onClick
|
onMouseLeave={() => {
|
||||||
? {
|
// Only clear timers, don't auto-hide
|
||||||
backgroundColor: 'background.level1',
|
if (hoverTimer) {
|
||||||
}
|
clearTimeout(hoverTimer)
|
||||||
: {},
|
setHoverTimer(null)
|
||||||
borderRadius: 'sm',
|
}
|
||||||
transition: 'background-color 0.2s',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 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>
|
<ListItemContent>
|
||||||
<Grid container spacing={1} alignItems='center'>
|
<Grid container spacing={1} alignItems='center'>
|
||||||
{/* First Row/Column: Status and Time Info */}
|
{/* First Row/Column: Status and Time Info */}
|
||||||
@@ -303,6 +561,56 @@ const HistoryCard = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ListItemContent>
|
</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>
|
</ListItem>
|
||||||
|
|
||||||
{/* Compact Divider with Time Difference */}
|
{/* Compact Divider with Time Difference */}
|
||||||
@@ -330,6 +638,7 @@ const HistoryCard = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</ListDivider>
|
</ListDivider>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
|
|||||||
onMouseLeave={handleActionAreaMouseLeave}
|
onMouseLeave={handleActionAreaMouseLeave}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='plain'
|
variant='soft'
|
||||||
color='neutral'
|
color='neutral'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -244,18 +244,13 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
mx: 1,
|
mx: 1,
|
||||||
bgcolor: 'primary.100',
|
|
||||||
color: 'primary.600',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'primary.200',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon sx={{ fontSize: 16 }} />
|
<EditIcon sx={{ fontSize: 16 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='plain'
|
variant='soft'
|
||||||
color='danger'
|
color='danger'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@@ -267,11 +262,6 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
mx: 1,
|
mx: 1,
|
||||||
bgcolor: 'danger.100',
|
|
||||||
color: 'danger.600',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'danger.200',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
<DeleteIcon sx={{ fontSize: 16 }} />
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ function EditHistoryModal({ config, historyRecord }) {
|
|||||||
const [notes, setNotes] = useState(historyRecord.notes)
|
const [notes, setNotes] = useState(historyRecord.notes)
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
<ResponsiveModal open={config?.isOpen} onClose={config?.onClose}>
|
<ResponsiveModal
|
||||||
|
open={config?.isOpen}
|
||||||
|
onClose={config?.onClose}
|
||||||
|
size='lg'
|
||||||
|
// fullWidth={true}
|
||||||
|
>
|
||||||
<Typography level='h4' mb={1}>
|
<Typography level='h4' mb={1}>
|
||||||
Edit History
|
Edit History
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -82,19 +87,9 @@ function EditHistoryModal({ config, historyRecord }) {
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='' onClick={config.onClose} variant='outlined'>
|
<Button fullWidth size='lg' onClick={config.onClose} variant='outlined'>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size='lg'
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(true)
|
|
||||||
}}
|
|
||||||
variant='outlined'
|
|
||||||
color='danger'
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
config={{
|
config={{
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Edit,
|
Edit,
|
||||||
PauseCircle,
|
PauseCircle,
|
||||||
|
Person,
|
||||||
PlayArrow,
|
PlayArrow,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -25,6 +27,7 @@ import {
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useCircleMembers } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
import {
|
import {
|
||||||
GetChoreTimer,
|
GetChoreTimer,
|
||||||
@@ -32,6 +35,7 @@ import {
|
|||||||
StartChore,
|
StartChore,
|
||||||
UpdateTimeSession,
|
UpdateTimeSession,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
|
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||||
|
|
||||||
const TimerDetails = () => {
|
const TimerDetails = () => {
|
||||||
const { choreId } = useParams()
|
const { choreId } = useParams()
|
||||||
@@ -50,6 +54,17 @@ const TimerDetails = () => {
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [isTouchDevice, setIsTouchDevice] = 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
|
// Detect if device supports touch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkTouchDevice = () => {
|
const checkTouchDevice = () => {
|
||||||
@@ -1230,16 +1245,56 @@ const TimerDetails = () => {
|
|||||||
>
|
>
|
||||||
{formatDuration(realTimeDuration)}
|
{formatDuration(realTimeDuration)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isOngoing && (
|
<Box
|
||||||
<Chip
|
sx={{ display: 'flex', gap: 0.5, mt: 0.5 }}
|
||||||
size='sm'
|
>
|
||||||
color='success'
|
{isOngoing && (
|
||||||
variant='soft'
|
<Chip
|
||||||
sx={{ fontSize: '0.7rem' }}
|
size='sm'
|
||||||
>
|
color='success'
|
||||||
Live
|
variant='soft'
|
||||||
</Chip>
|
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>
|
</Box>
|
||||||
|
|
||||||
{/* Session details */}
|
{/* Session details */}
|
||||||
|
|||||||
Reference in New Issue
Block a user