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:
Mo Tarbin
2025-08-17 22:27:58 -04:00
parent ca9a53bde9
commit 6eda3c68a8
11 changed files with 882 additions and 353 deletions

View File

@@ -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'

View File

@@ -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'}

View File

@@ -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}

View File

@@ -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 */}

View File

@@ -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>
</>
)

View File

@@ -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' }}>

View File

@@ -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 />}>

View File

@@ -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>

View File

@@ -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',
}}
>

View File

@@ -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 */}

View File

@@ -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={{