Refactor chore management features to support approval and rejection workflows
- Updated RepeatSection to use occurrences instead of dayOccurrences for scheduling. - Enhanced ChoreCard and CompactChoreCard to include approve/reject functionality for pending chores. - Implemented user role checks for approval/rejection capabilities. - Improved UI elements to reflect pending approval states with appropriate icons. - Modified ChoreHistory and ThingsHistory components for better data representation and layout. - Cleaned up unused imports and commented-out code for better readability.
This commit is contained in:
@@ -6,12 +6,15 @@ import {
|
||||
CloseFullscreen,
|
||||
Edit,
|
||||
History,
|
||||
HourglassEmpty,
|
||||
LowPriority,
|
||||
OpenInFull,
|
||||
PeopleAlt,
|
||||
Person,
|
||||
PlayArrow,
|
||||
SwitchAccessShortcut,
|
||||
ThumbDown,
|
||||
ThumbUp,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -41,15 +44,17 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
||||
import { useCircleMembers } from '../../queries/UserQueries.jsx'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteTimeSession,
|
||||
GetChoreDetailById,
|
||||
GetChoreTimer,
|
||||
MarkChoreComplete,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
ResetChoreTimer,
|
||||
SkipChore,
|
||||
StartChore,
|
||||
@@ -84,6 +89,7 @@ const ChoreView = () => {
|
||||
const [timerActionConfig, setTimerActionConfig] = useState({})
|
||||
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
|
||||
useCircleMembers()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
const { data: choreData, isLoading: isChoreLoading } =
|
||||
@@ -320,6 +326,48 @@ const ChoreView = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveChore = () => {
|
||||
ApproveChore(choreId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
setChore(data.res)
|
||||
// Invalidate chores cache to refetch data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectChore = () => {
|
||||
RejectChore(choreId).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
setChore(data.res)
|
||||
// Invalidate chores cache to refetch data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
if (!circleMembersData?.res || !chore) return false
|
||||
|
||||
const currentUser = circleMembersData.res.find(
|
||||
member => member.userId === (impersonatedUser?.userId || userProfile?.id),
|
||||
)
|
||||
|
||||
// User can approve/reject if they are:
|
||||
// 1. Admin or manager of the circle
|
||||
// 2. Owner/creator of the task
|
||||
return (
|
||||
currentUser?.role === 'admin' ||
|
||||
currentUser?.role === 'manager' ||
|
||||
chore.createdBy === (impersonatedUser?.userId || userProfile?.id)
|
||||
)
|
||||
}
|
||||
|
||||
if (isChoreLoading || isCircleMembersLoading) {
|
||||
// while loading the chore or circle members, return a loading state
|
||||
return <LoadingComponent />
|
||||
@@ -396,7 +444,7 @@ const ChoreView = () => {
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{chore.status !== 0 && (
|
||||
{[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) && (
|
||||
<Grid item xs={12}>
|
||||
<TimePassedCard
|
||||
chore={chore}
|
||||
@@ -665,7 +713,7 @@ const ChoreView = () => {
|
||||
variant='soft'
|
||||
>
|
||||
<Typography level='body-md' sx={{ mb: 1 }}>
|
||||
Completion options
|
||||
Task Actions
|
||||
</Typography>
|
||||
|
||||
<FormControl size='sm'>
|
||||
@@ -770,59 +818,104 @@ const ChoreView = () => {
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={handleTaskCompletion}
|
||||
disabled={
|
||||
isPendingCompletion ||
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
}
|
||||
color={isPendingCompletion ? 'danger' : 'success'}
|
||||
startDecorator={<Check />}
|
||||
sx={{
|
||||
flex: 4,
|
||||
}}
|
||||
>
|
||||
<Box>Mark as done</Box>
|
||||
</Button>
|
||||
{chore.status === 3 ? (
|
||||
// Pending approval: Show approve/reject for admins/managers/owners, grayed out button for others
|
||||
canApproveReject() ? (
|
||||
<>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={handleApproveChore}
|
||||
color='success'
|
||||
startDecorator={<ThumbUp />}
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={handleRejectChore}
|
||||
color='danger'
|
||||
startDecorator={<ThumbDown />}
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box>Reject</Box>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
disabled={true}
|
||||
color='neutral'
|
||||
startDecorator={<HourglassEmpty />}
|
||||
>
|
||||
<Box>Pending Approval</Box>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
// Normal completion flow
|
||||
<>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={handleTaskCompletion}
|
||||
disabled={
|
||||
isPendingCompletion ||
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
}
|
||||
color={isPendingCompletion ? 'danger' : 'success'}
|
||||
startDecorator={<Check />}
|
||||
sx={{
|
||||
flex: 4,
|
||||
}}
|
||||
>
|
||||
<Box>Mark as done</Box>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Skip Task',
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
onClick={() => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Skip Task',
|
||||
|
||||
message: 'Are you sure you want to skip this task?',
|
||||
message: 'Are you sure you want to skip this task?',
|
||||
|
||||
confirmText: 'Skip',
|
||||
cancelText: 'Cancel',
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
handleSkippingTask()
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}}
|
||||
disabled={
|
||||
chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once'
|
||||
}
|
||||
startDecorator={<SwitchAccessShortcut />}
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box>Skip</Box>
|
||||
</Button>
|
||||
confirmText: 'Skip',
|
||||
cancelText: 'Cancel',
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
handleSkippingTask()
|
||||
}
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}}
|
||||
disabled={
|
||||
chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once'
|
||||
}
|
||||
startDecorator={<SwitchAccessShortcut />}
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box>Skip</Box>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{/* Timer Button - Show split button when timer is active, regular button otherwise */}
|
||||
{chore.status !== 0 ? (
|
||||
{[ChoreStatus.ACTIVE, ChoreStatus.PAUSED].includes(chore.status) ? (
|
||||
<TimerSplitButton
|
||||
disabled={
|
||||
chore.lastCompletedDate !== null &&
|
||||
@@ -841,6 +934,8 @@ const ChoreView = () => {
|
||||
onClearAllTime={handleClearAllTime}
|
||||
fullWidth
|
||||
/>
|
||||
) : chore.status === ChoreStatus.PENDING_APPROVAL ? (
|
||||
<></>
|
||||
) : (
|
||||
<Button
|
||||
size='lg'
|
||||
|
||||
@@ -93,9 +93,9 @@ const generateSchedulePreview = metadata => {
|
||||
|
||||
if (
|
||||
metadata.weekPattern === 'nth_day_of_month' &&
|
||||
metadata.dayOccurrences?.length
|
||||
metadata.occurrences?.length
|
||||
) {
|
||||
const occurrenceStr = metadata.dayOccurrences
|
||||
const occurrenceStr = metadata.occurrences
|
||||
.map(w => {
|
||||
if (w === -1) return 'last'
|
||||
return `${w}${w === 1 ? 'st' : w === 2 ? 'nd' : w === 3 ? 'rd' : 'th'}`
|
||||
@@ -127,7 +127,7 @@ const RepeatOnSections = ({
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
weekPattern: 'every_week',
|
||||
dayOccurrences: [],
|
||||
occurrences: [],
|
||||
})
|
||||
}
|
||||
}, [frequencyMetadata, onFrequencyMetadataUpdate])
|
||||
@@ -255,7 +255,7 @@ const RepeatOnSections = ({
|
||||
...frequencyMetadata,
|
||||
days: [],
|
||||
weekPattern: 'every_week',
|
||||
dayOccurrences: [],
|
||||
occurrences: [],
|
||||
})
|
||||
} else {
|
||||
onFrequencyMetadataUpdate({
|
||||
@@ -283,10 +283,10 @@ const RepeatOnSections = ({
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
weekPattern: newPattern,
|
||||
dayOccurrences:
|
||||
occurrences:
|
||||
newPattern === 'every_week'
|
||||
? []
|
||||
: frequencyMetadata?.dayOccurrences || [],
|
||||
: frequencyMetadata?.occurrences || [],
|
||||
})
|
||||
}}
|
||||
sx={{ gap: 1, '& > div': { p: 1 } }}
|
||||
@@ -330,13 +330,13 @@ const RepeatOnSections = ({
|
||||
<ListItem key={option.value}>
|
||||
<Checkbox
|
||||
checked={
|
||||
frequencyMetadata?.dayOccurrences?.includes(
|
||||
frequencyMetadata?.occurrences?.includes(
|
||||
option.value,
|
||||
) || false
|
||||
}
|
||||
onChange={() => {
|
||||
const currentOccurrences =
|
||||
frequencyMetadata?.dayOccurrences || []
|
||||
frequencyMetadata?.occurrences || []
|
||||
const newOccurrences =
|
||||
currentOccurrences.includes(option.value)
|
||||
? currentOccurrences.filter(
|
||||
@@ -345,7 +345,7 @@ const RepeatOnSections = ({
|
||||
: [...currentOccurrences, option.value]
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
dayOccurrences: newOccurrences.sort((a, b) => {
|
||||
occurrences: newOccurrences.sort((a, b) => {
|
||||
if (a === -1) return 1 // Last occurrence goes to end
|
||||
if (b === -1) return -1
|
||||
return a - b
|
||||
@@ -366,17 +366,17 @@ const RepeatOnSections = ({
|
||||
color='neutral'
|
||||
onClick={() => {
|
||||
if (
|
||||
frequencyMetadata?.dayOccurrences?.length ===
|
||||
frequencyMetadata?.occurrences?.length ===
|
||||
DAY_OCCURRENCE_OPTIONS.length
|
||||
) {
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
dayOccurrences: [],
|
||||
occurrences: [],
|
||||
})
|
||||
} else {
|
||||
onFrequencyMetadataUpdate({
|
||||
...frequencyMetadata,
|
||||
dayOccurrences: DAY_OCCURRENCE_OPTIONS.map(
|
||||
occurrences: DAY_OCCURRENCE_OPTIONS.map(
|
||||
option => option.value,
|
||||
),
|
||||
})
|
||||
@@ -385,7 +385,7 @@ const RepeatOnSections = ({
|
||||
overlay
|
||||
disableIcon
|
||||
>
|
||||
{frequencyMetadata?.dayOccurrences?.length ===
|
||||
{frequencyMetadata?.occurrences?.length ===
|
||||
DAY_OCCURRENCE_OPTIONS.length
|
||||
? 'Unselect All'
|
||||
: 'Select All'}
|
||||
|
||||
@@ -3,10 +3,13 @@ import {
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
HourglassEmpty,
|
||||
Pause,
|
||||
PlayArrow,
|
||||
Repeat,
|
||||
Schedule,
|
||||
ThumbDown,
|
||||
ThumbUp,
|
||||
TimesOneMobiledata,
|
||||
Toll,
|
||||
Webhook,
|
||||
@@ -28,14 +31,16 @@ import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
StartChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
@@ -76,6 +81,7 @@ const ChoreCard = ({
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
@@ -234,6 +240,46 @@ const ChoreCard = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
ApproveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'approved')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectChore = () => {
|
||||
resetSwipe()
|
||||
RejectChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'rejected')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
if (!circleMembersData?.res || !chore) return false
|
||||
|
||||
const currentUser = circleMembersData.res.find(
|
||||
member => member.userId === (impersonatedUser?.userId || userProfile?.id),
|
||||
)
|
||||
|
||||
// User can approve/reject if they are:
|
||||
// 1. Admin or manager of the circle
|
||||
// 2. Owner/creator of the task
|
||||
return (
|
||||
currentUser?.role === 'admin' ||
|
||||
currentUser?.role === 'manager' ||
|
||||
chore.createdBy === (impersonatedUser?.userId || userProfile?.id)
|
||||
)
|
||||
}
|
||||
|
||||
// Swipe gesture handlers
|
||||
const handleTouchStart = e => {
|
||||
if (isMultiSelectMode || viewOnly) return
|
||||
@@ -642,32 +688,80 @@ const ChoreCard = ({
|
||||
onMouseEnter={handleActionAreaMouseEnter}
|
||||
onMouseLeave={handleActionAreaMouseLeave}
|
||||
>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='md'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status !== 0) {
|
||||
handleTaskCompletion()
|
||||
} else {
|
||||
handleChoreStart()
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
{chore.status !== 0 ? (
|
||||
<Check sx={{ fontSize: 20 }} />
|
||||
{chore.status === 3 ? (
|
||||
// Pending approval: Show approve/reject for admins/managers/owners
|
||||
canApproveReject() ? (
|
||||
<>
|
||||
{/* <IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='md'
|
||||
onClick={handleApproveChore}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<ThumbUp sx={{ fontSize: 20 }} />
|
||||
</IconButton> */}
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='danger'
|
||||
size='md'
|
||||
onClick={handleRejectChore}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<ThumbDown sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<PlayArrow sx={{ fontSize: 20 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='md'
|
||||
disabled={true}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<HourglassEmpty sx={{ fontSize: 20 }} />
|
||||
</IconButton>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='md'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status !== 0) {
|
||||
handleTaskCompletion()
|
||||
} else {
|
||||
handleChoreStart()
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
{chore.status !== 0 ? (
|
||||
<Check sx={{ fontSize: 20 }} />
|
||||
) : (
|
||||
<PlayArrow sx={{ fontSize: 20 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
variant='soft'
|
||||
@@ -949,68 +1043,143 @@ const ChoreCard = ({
|
||||
alignItems='flex-end'
|
||||
>
|
||||
{/* <ButtonGroup> */}
|
||||
<IconButton
|
||||
variant={chore.status === 0 ? 'solid' : 'soft'}
|
||||
color={chore.status === 0 ? 'success' : 'warning'}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
switch (chore.status) {
|
||||
case 0: // Not started
|
||||
handleTaskCompletion()
|
||||
break
|
||||
case 1: // In progress
|
||||
handleChorePause()
|
||||
break
|
||||
case 2: // Paused
|
||||
handleChoreStart()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}}
|
||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
transform: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className='relative grid place-items-center'>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='md' />
|
||||
) : chore.status === 0 ? (
|
||||
<Check />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause />
|
||||
) : (
|
||||
<PlayArrow />
|
||||
)}
|
||||
{isPendingCompletion && (
|
||||
<CircularProgress
|
||||
variant='solid'
|
||||
{chore.status === 3 ? (
|
||||
// Pending approval: Show approve/reject for admins/managers/owners, grayed out for others
|
||||
canApproveReject() ? (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='md'
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
position: 'absolute',
|
||||
zIndex: 0,
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleApproveChore()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</IconButton>
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
transform: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThumbUp sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
{/* <IconButton
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleRejectChore()
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 40,
|
||||
height: 40,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThumbDown sx={{ fontSize: 18 }} />
|
||||
</IconButton> */}
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
disabled={true}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<HourglassEmpty />
|
||||
</IconButton>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
variant={chore.status === 0 ? 'solid' : 'soft'}
|
||||
color={chore.status === 0 ? 'success' : 'warning'}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
switch (chore.status) {
|
||||
case 0: // Not started
|
||||
handleTaskCompletion()
|
||||
break
|
||||
case 1: // In progress
|
||||
handleChorePause()
|
||||
break
|
||||
case 2: // Paused
|
||||
handleChoreStart()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isPendingCompletion || notInCompletionWindow(chore)
|
||||
}
|
||||
sx={{
|
||||
borderRadius: '50%',
|
||||
minWidth: 50,
|
||||
height: 50,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
transform: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className='relative grid place-items-center'>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='md' />
|
||||
) : chore.status === 0 ? (
|
||||
<Check />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause />
|
||||
) : (
|
||||
<PlayArrow />
|
||||
)}
|
||||
{isPendingCompletion && (
|
||||
<CircularProgress
|
||||
variant='solid'
|
||||
color='success'
|
||||
size='md'
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
position: 'absolute',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</IconButton>
|
||||
)}
|
||||
<ChoreActionMenu
|
||||
chore={chore}
|
||||
onChoreUpdate={onChoreUpdate}
|
||||
|
||||
@@ -3,10 +3,13 @@ import {
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
HourglassEmpty,
|
||||
Pause,
|
||||
PlayArrow,
|
||||
Repeat,
|
||||
Schedule,
|
||||
ThumbDown,
|
||||
ThumbUp,
|
||||
TimesOneMobiledata,
|
||||
Webhook,
|
||||
} from '@mui/icons-material'
|
||||
@@ -24,7 +27,7 @@ import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import {
|
||||
@@ -32,9 +35,11 @@ import {
|
||||
TASK_COLOR,
|
||||
} from '../../utils/Colors.jsx'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
PauseChore,
|
||||
RejectChore,
|
||||
StartChore,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
@@ -75,6 +80,7 @@ const CompactChoreCard = ({
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
|
||||
const [timeoutId, setTimeoutId] = React.useState(null)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
@@ -407,6 +413,46 @@ const CompactChoreCard = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveChore = () => {
|
||||
resetSwipe()
|
||||
ApproveChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'approved')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRejectChore = () => {
|
||||
resetSwipe()
|
||||
RejectChore(chore.id).then(response => {
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
onChoreUpdate(data.res, 'rejected')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the current user can approve/reject (admin, manager, or task owner)
|
||||
const canApproveReject = () => {
|
||||
if (!circleMembersData?.res || !chore) return false
|
||||
|
||||
const currentUser = circleMembersData.res.find(
|
||||
member => member.userId === (impersonatedUser?.userId || userProfile?.id),
|
||||
)
|
||||
|
||||
// User can approve/reject if they are:
|
||||
// 1. Admin or manager of the circle
|
||||
// 2. Owner/creator of the task
|
||||
return (
|
||||
currentUser?.role === 'admin' ||
|
||||
currentUser?.role === 'manager' ||
|
||||
chore.createdBy === (impersonatedUser?.userId || userProfile?.id)
|
||||
)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const getDueDateText = nextDueDate => {
|
||||
if (chore.nextDueDate === null) return 'No Due Date'
|
||||
@@ -641,38 +687,81 @@ const CompactChoreCard = ({
|
||||
onMouseEnter={handleActionAreaMouseEnter}
|
||||
onMouseLeave={handleActionAreaMouseLeave}
|
||||
>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status === 0 || chore.status === 2) {
|
||||
handleChoreStart()
|
||||
} else {
|
||||
// handleChorePause()
|
||||
handleTaskCompletion()
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
// bgcolor: 'success.100',
|
||||
// color: 'success.600',
|
||||
// '&:hover': {
|
||||
// bgcolor: 'success.200',
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{chore.status !== 1 ? (
|
||||
<PlayArrow sx={{ fontSize: 16 }} />
|
||||
{chore.status === 3 ? (
|
||||
// Pending approval: Show approve/reject for admins/managers/owners
|
||||
canApproveReject() ? (
|
||||
<>
|
||||
{/* <IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={handleApproveChore}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<ThumbUp sx={{ fontSize: 16 }} />
|
||||
</IconButton> */}
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='danger'
|
||||
size='sm'
|
||||
onClick={handleRejectChore}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<ThumbDown sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
disabled={true}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<HourglassEmpty sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
resetSwipe()
|
||||
|
||||
if (chore.status === 0 || chore.status === 2) {
|
||||
handleChoreStart()
|
||||
} else {
|
||||
// handleChorePause()
|
||||
handleTaskCompletion()
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
{chore.status !== 1 ? (
|
||||
<PlayArrow sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
variant='soft'
|
||||
@@ -843,49 +932,118 @@ const CompactChoreCard = ({
|
||||
pointerEvents: isMultiSelectMode ? 'none' : 'auto',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color={chore.status === 0 ? 'success' : 'warning'}
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
if (chore.status === 0) {
|
||||
handleTaskCompletion()
|
||||
} else if (chore.status === 1) {
|
||||
handleChorePause()
|
||||
} else {
|
||||
handleChoreStart()
|
||||
}
|
||||
}}
|
||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
transform: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='sm' />
|
||||
) : chore.status === 0 ? (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause sx={{ fontSize: 16 }} />
|
||||
{chore.status === 3 ? (
|
||||
// Pending approval: Show approve/reject for admins/managers/owners, grayed out for others
|
||||
canApproveReject() ? (
|
||||
<Box sx={{ display: 'flex', gap: 0.25 }}>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleApproveChore()
|
||||
}}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThumbUp sx={{ fontSize: 12 }} />
|
||||
</IconButton>
|
||||
{/* <IconButton
|
||||
variant='soft'
|
||||
color='danger'
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
handleRejectChore()
|
||||
}}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThumbDown sx={{ fontSize: 12 }} />
|
||||
</IconButton> */}
|
||||
</Box>
|
||||
) : (
|
||||
<PlayArrow sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
disabled={true}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<HourglassEmpty sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
variant='soft'
|
||||
color={chore.status === 0 ? 'success' : 'warning'}
|
||||
size='sm'
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
if (chore.status === 0) {
|
||||
handleTaskCompletion()
|
||||
} else if (chore.status === 1) {
|
||||
handleChorePause()
|
||||
} else {
|
||||
handleChoreStart()
|
||||
}
|
||||
}}
|
||||
disabled={isPendingCompletion || notInCompletionWindow(chore)}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
transform: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isPendingCompletion ? (
|
||||
<CircularProgress size='sm' />
|
||||
) : chore.status === 0 ? (
|
||||
<Check sx={{ fontSize: 16 }} />
|
||||
) : chore.status === 1 ? (
|
||||
<Pause sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<PlayArrow sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Multi-select Checkbox */}
|
||||
|
||||
@@ -145,7 +145,7 @@ const SortAndGrouping = ({
|
||||
|
||||
<MenuItem key={`${k}-assignee-title`} disabled>
|
||||
<Typography level='body-xs' fontWeight='md'>
|
||||
Assigned to:
|
||||
Assigned to :
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
|
||||
@@ -174,6 +174,20 @@ const SortAndGrouping = ({
|
||||
<Typography level='body-sm'>Assigned to me</Typography>
|
||||
</MenuItem>
|
||||
|
||||
{/* <MenuItem
|
||||
key={`${k}-assignee-assignable-to-me`}
|
||||
onClick={() => {
|
||||
setFilter('assignable_to_me')
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'assignable_to_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Available for me</Typography>
|
||||
</MenuItem> */}
|
||||
|
||||
<MenuItem
|
||||
key={`${k}-assignee-assigned-to-others`}
|
||||
onClick={() => {
|
||||
@@ -187,6 +201,21 @@ const SortAndGrouping = ({
|
||||
/>
|
||||
<Typography level='body-sm'>Assigned to others</Typography>
|
||||
</MenuItem>
|
||||
{/*
|
||||
// i need this but i think it have a bad UX and confusing so commenting it for now
|
||||
<MenuItem
|
||||
key={`${k}-assignee-created-by-me`}
|
||||
onClick={() => {
|
||||
setFilter('created_by_me')
|
||||
handleMenuClose()
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedFilter === 'created_by_me'}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography level='body-sm'>Created by me</Typography>
|
||||
</MenuItem> */}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Checklist,
|
||||
EventBusy,
|
||||
Group,
|
||||
History,
|
||||
Star,
|
||||
Timelapse,
|
||||
TrendingUp,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Container,
|
||||
Grid,
|
||||
List,
|
||||
@@ -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 { ChoreHistoryStatus } from '../../utils/Chores'
|
||||
import {
|
||||
DeleteChoreHistory,
|
||||
GetAllCircleMembers,
|
||||
@@ -98,15 +99,12 @@ const ChoreHistory = () => {
|
||||
const userCompletedByMost = Object.keys(userHistories).reduce((a, b) =>
|
||||
userHistories[a] > userHistories[b] ? a : b,
|
||||
)
|
||||
const userCompletedByLeast = Object.keys(userHistories).reduce((a, b) =>
|
||||
userHistories[a] < userHistories[b] ? a : b,
|
||||
)
|
||||
|
||||
const historyInfo = [
|
||||
{
|
||||
icon: <Checklist />,
|
||||
text: 'Total Completed',
|
||||
subtext: `${histories.length} times`,
|
||||
text: 'All Completed',
|
||||
subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED).length} times`,
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp />,
|
||||
@@ -117,14 +115,14 @@ const ChoreHistory = () => {
|
||||
},
|
||||
{
|
||||
icon: <Timelapse />,
|
||||
text: 'Maximum Delay',
|
||||
text: 'Longest Delay',
|
||||
subtext: moment.duration(maxDelayMoment).isValid()
|
||||
? moment.duration(maxDelayMoment).humanize()
|
||||
: 'Never late',
|
||||
},
|
||||
{
|
||||
icon: <Star />,
|
||||
text: 'Top Performer',
|
||||
text: 'Completed Most',
|
||||
subtext: `${
|
||||
performers.find(p => p.userId === Number(userCompletedByMost))
|
||||
?.displayName || 'Unknown'
|
||||
@@ -132,12 +130,12 @@ const ChoreHistory = () => {
|
||||
},
|
||||
{
|
||||
icon: <Group />,
|
||||
text: 'Team Members',
|
||||
subtext: `${Object.keys(userHistories).length} active`,
|
||||
text: 'Members Involved',
|
||||
subtext: `${Object.keys(userHistories).length} members`,
|
||||
},
|
||||
{
|
||||
icon: <Analytics />,
|
||||
text: 'Last Completed By',
|
||||
text: 'Last Completed',
|
||||
subtext: `${
|
||||
performers.find(p => p.userId === Number(histories[0].completedBy))
|
||||
?.displayName || 'Unknown'
|
||||
@@ -191,52 +189,76 @@ const ChoreHistory = () => {
|
||||
<Container maxWidth='md'>
|
||||
{/* Enhanced Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
{/* Statistics Cards Grid */}
|
||||
<Grid container spacing={1} sx={{ mb: 1 }}>
|
||||
{/* Statistics Cards Grid - Compact Design */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<History sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Task Summary
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={0.5} sx={{ mb: 2 }}>
|
||||
{historyInfo.map((info, index) => (
|
||||
<Grid item xs={6} sm={6} key={index}>
|
||||
<Grid item xs={4} sm={2} key={index}>
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderRadius: 'md',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
borderRadius: 'sm',
|
||||
p: 1,
|
||||
height: 85,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box
|
||||
<Box sx={{ opacity: 0.8, flexShrink: 0 }}>{info.icon}</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
fontWeight: '600',
|
||||
color: 'text.primary',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
>
|
||||
{info.icon}
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
ml: 1,
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{info.text}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
||||
>
|
||||
{info.subtext || '--'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
{info.text}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
{info.subtext || '--'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
@@ -245,9 +267,12 @@ const ChoreHistory = () => {
|
||||
|
||||
{/* History Section Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
||||
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
||||
Completion History
|
||||
<Analytics sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Task Activity
|
||||
</Typography>
|
||||
</Box>
|
||||
<Sheet variant='plain' sx={{ borderRadius: 'sm', boxShadow: 'md' }}>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
Check,
|
||||
CheckCircle,
|
||||
EventNote,
|
||||
HourglassEmpty,
|
||||
Person,
|
||||
Redo,
|
||||
ThumbDown,
|
||||
Timelapse,
|
||||
Toll,
|
||||
} from '@mui/icons-material'
|
||||
@@ -121,6 +123,8 @@ const HistoryCard = ({
|
||||
0: { icon: <AccessTime />, color: 'primary' }, // Started
|
||||
1: { icon: <Check />, color: 'success' }, // Completed
|
||||
2: { icon: <Redo />, color: 'warning' }, // Skipped
|
||||
3: { icon: <HourglassEmpty />, color: 'neutral' }, // Pending Approval
|
||||
4: { icon: <ThumbDown />, color: 'danger' }, // Rejected
|
||||
}
|
||||
|
||||
const config = statusMap[historyEntry.status] || statusMap[1]
|
||||
@@ -182,7 +186,13 @@ const HistoryCard = ({
|
||||
? 'In Progress'
|
||||
: historyEntry.status === 1
|
||||
? 'Completed'
|
||||
: 'Skipped'}
|
||||
: historyEntry.status === 2
|
||||
? 'Skipped'
|
||||
: historyEntry.status === 3
|
||||
? 'Pending Approval'
|
||||
: historyEntry.status === 4
|
||||
? 'Rejected'
|
||||
: 'Completed'}
|
||||
</Typography>
|
||||
|
||||
<Chip size='sm' startDecorator={<EventNote />}>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Container,
|
||||
Grid,
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
ListDivider,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useTheme } from '@mui/joy/styles'
|
||||
@@ -188,68 +186,78 @@ const ThingsHistory = () => {
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
{/* Enhanced Analytics Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<BarChart sx={{ fontSize: '2rem', color: 'primary.500' }} />
|
||||
<Stack>
|
||||
<Typography
|
||||
level='h3'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Things Details
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Quick overview of the thing's history and analytics
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<BarChart sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Things Overview
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Statistics Cards Grid */}
|
||||
<Grid container spacing={1} sx={{ mb: 1 }}>
|
||||
{/* Statistics Cards Grid - Compact Design */}
|
||||
<Grid container spacing={0.5} sx={{ mb: 2 }}>
|
||||
{analyticsData.map((info, index) => (
|
||||
<Grid xs={6} sm={6} key={index}>
|
||||
<Grid xs={6} sm={3} key={index}>
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderRadius: 'md',
|
||||
boxShadow: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
minHeight: 90,
|
||||
height: '100%',
|
||||
justifyContent: 'start',
|
||||
borderRadius: 'sm',
|
||||
p: 1,
|
||||
height: 85,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box
|
||||
<Box sx={{ opacity: 0.8, flexShrink: 0 }}>{info.icon}</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
mb: 0.5,
|
||||
fontWeight: '600',
|
||||
color: 'text.primary',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
>
|
||||
{info.icon}
|
||||
<Typography
|
||||
level='body-md'
|
||||
sx={{
|
||||
ml: 1,
|
||||
fontWeight: '500',
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{info.text}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'text.secondary', lineHeight: 1.5 }}
|
||||
>
|
||||
{info.subtext || '--'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
{info.text}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
{info.subtext || '--'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
@@ -260,9 +268,9 @@ const ThingsHistory = () => {
|
||||
{thingsHistory.every(history => !isNaN(history.state)) &&
|
||||
thingsHistory.length > 1 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Analytics sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
||||
<Analytics sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='h4'
|
||||
level='title-md'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Data Visualization
|
||||
@@ -324,9 +332,13 @@ const ThingsHistory = () => {
|
||||
)}
|
||||
|
||||
{/* History Section Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<Timeline sx={{ fontSize: '1.5rem', color: 'primary.500' }} />
|
||||
<Typography level='h4' sx={{ fontWeight: 'lg', color: 'text.primary' }}>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Timeline sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Change History
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -662,10 +662,6 @@ const ThingsView = () => {
|
||||
<Container maxWidth='md' sx={{ px: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
// bgcolor: 'background.body',
|
||||
// border: '1px solid',
|
||||
// borderColor: 'divider',
|
||||
// borderRadius: 'md',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import CircleIcon from '@mui/icons-material/Circle'
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'
|
||||
import ThumbDownIcon from '@mui/icons-material/ThumbDown'
|
||||
import TimelapseIcon from '@mui/icons-material/Timelapse'
|
||||
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
|
||||
|
||||
import { EventBusy, Group, Timeline, Toll } from '@mui/icons-material'
|
||||
import {
|
||||
Block,
|
||||
Check,
|
||||
EventBusy,
|
||||
Group,
|
||||
Timeline,
|
||||
Toll,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -44,11 +52,22 @@ const groupByDate = history => {
|
||||
return aggregated
|
||||
}
|
||||
|
||||
const ChoreHistoryItem = ({ time, name, points, status }) => {
|
||||
const statusIcon = {
|
||||
completed: <CheckCircleIcon color='success' />,
|
||||
missed: <CancelIcon color='error' />,
|
||||
pending: <CircleIcon color='neutral' />,
|
||||
const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
|
||||
const getStatusIcon = status => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return <TimelapseIcon color='primary' />
|
||||
case 1:
|
||||
return <Check color='success' />
|
||||
case 2:
|
||||
return <Block color='warning' />
|
||||
case 3:
|
||||
return <HourglassEmptyIcon color='action' />
|
||||
case 4:
|
||||
return <ThumbDownIcon color='error' />
|
||||
default:
|
||||
return <CheckCircleIcon color='success' />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -56,15 +75,25 @@ const ChoreHistoryItem = ({ time, name, points, status }) => {
|
||||
<Typography level='body-md' sx={{ minWidth: 80 }}>
|
||||
{time}
|
||||
</Typography>
|
||||
<Box>
|
||||
{statusIcon[status] ? statusIcon[status] : statusIcon['completed']}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'background.level2',
|
||||
boxShadow: 'sm',
|
||||
}}
|
||||
>
|
||||
{getStatusIcon(status)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 40,
|
||||
// center vertically:
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -795,7 +824,6 @@ const UserActivites = () => {
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
AccountBalanceWallet,
|
||||
Analytics,
|
||||
AssignmentTurnedIn,
|
||||
CreditCard,
|
||||
EmojiEvents,
|
||||
MilitaryTech,
|
||||
@@ -530,7 +531,13 @@ const UserPoints = () => {
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
||||
>
|
||||
<Toll sx={{ fontSize: '1rem', color: 'success.500' }} />
|
||||
{leaderboardMode === 'points' ? (
|
||||
<Toll sx={{ fontSize: '1rem', color: 'success.500' }} />
|
||||
) : (
|
||||
<AssignmentTurnedIn
|
||||
sx={{ fontSize: '1rem', color: 'success.500' }}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{
|
||||
|
||||
Reference in New Issue
Block a user