refactor: Consolidate chore action handling into ChoreActionMenu component and remove unused code from ChoreCard and CompactChoreCard

This commit is contained in:
Mo Tarbin
2025-06-01 00:47:47 -04:00
parent cf252c3ec4
commit ddf654a641
6 changed files with 351 additions and 457 deletions

View File

@@ -30,7 +30,6 @@ import {
MenuItem, MenuItem,
Sheet, Sheet,
Snackbar, Snackbar,
styled,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import { Divider } from '@mui/material' import { Divider } from '@mui/material'
@@ -54,16 +53,6 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import LoadingComponent from '../components/Loading.jsx' import LoadingComponent from '../components/Loading.jsx'
import RichTextEditor from '../components/RichTextEditor.jsx' import RichTextEditor from '../components/RichTextEditor.jsx'
import SubTasks from '../components/SubTask.jsx' import SubTasks from '../components/SubTask.jsx'
const IconCard = styled('div')({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f0f0f0', // Adjust the background color as needed
borderRadius: '50%',
minWidth: '40px',
height: '40px',
marginRight: '16px',
})
const ChoreView = () => { const ChoreView = () => {
const [chore, setChore] = useState({}) const [chore, setChore] = useState({})

View File

@@ -1,29 +1,9 @@
import { import {
Archive,
CancelScheduleSend, CancelScheduleSend,
Check, Check,
CopyAll,
Delete,
Edit,
HorizontalRule,
KeyboardControlKey,
KeyboardDoubleArrowUp,
LocalOffer,
ManageSearch,
MoreTime,
MoreVert,
Nfc,
NoteAdd,
PriorityHigh,
RecordVoiceOver,
Repeat, Repeat,
Report,
SwitchAccessShortcut,
TimesOneMobiledata, TimesOneMobiledata,
Toll, Toll,
Unarchive,
Update,
ViewCarousel,
Webhook, Webhook,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@@ -33,16 +13,13 @@ import {
Card, Card,
Chip, Chip,
CircularProgress, CircularProgress,
Divider,
Grid, Grid,
IconButton, IconButton,
Menu,
MenuItem,
Snackbar, Snackbar,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import React, { useEffect } from 'react' import React from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
@@ -50,11 +27,8 @@ import { useError } from '../../service/ErrorProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { import {
ArchiveChore,
DeleteChore, DeleteChore,
MarkChoreComplete, MarkChoreComplete,
SkipChore,
UnArchiveChore,
UpdateChoreAssignee, UpdateChoreAssignee,
UpdateDueDate, UpdateDueDate,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
@@ -64,17 +38,16 @@ import DateModal from '../Modals/Inputs/DateModal'
import SelectModal from '../Modals/Inputs/SelectModal' import SelectModal from '../Modals/Inputs/SelectModal'
import TextModal from '../Modals/Inputs/TextModal' import TextModal from '../Modals/Inputs/TextModal'
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal' import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
import ChoreActionMenu from '../components/ChoreActionMenu'
const ChoreCard = ({ const ChoreCard = ({
chore, chore,
performers, performers,
onChoreUpdate, onChoreUpdate,
onChoreRemove, onChoreRemove,
userLabels,
sx, sx,
viewOnly, viewOnly,
onChipClick, onChipClick,
}) => { }) => {
const [activeUserId, setActiveUserId] = React.useState(0)
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] = const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
React.useState(false) React.useState(false)
const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] = const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
@@ -85,10 +58,7 @@ const ChoreCard = ({
React.useState(false) React.useState(false)
const [confirmModelConfig, setConfirmModelConfig] = React.useState({}) const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false) const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null)
const navigate = useNavigate() const navigate = useNavigate()
const [isDisabled, setIsDisabled] = React.useState(false)
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false) const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null) const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
@@ -97,39 +67,7 @@ const ChoreCard = ({
const { impersonatedUser } = useImpersonateUser() const { impersonatedUser } = useImpersonateUser()
const { showError } = useError() const { showError } = useError()
useEffect(() => {
document.addEventListener('mousedown', handleMenuOutsideClick)
return () => {
document.removeEventListener('mousedown', handleMenuOutsideClick)
}
}, [anchorEl])
const handleMenuOpen = event => {
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
}
const handleMenuOutsideClick = event => {
if (
anchorEl &&
!anchorEl.contains(event.target) &&
!menuRef.current.contains(event.target)
) {
handleMenuClose()
}
}
const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`)
}
const handleClone = () => {
navigate(`/chores/${chore.id}/edit?clone=true`)
}
const handleView = () => {
navigate(`/chores/${chore.id}`)
}
const handleDelete = () => { const handleDelete = () => {
setConfirmModelConfig({ setConfirmModelConfig({
isOpen: true, isOpen: true,
@@ -149,30 +87,6 @@ const ChoreCard = ({
}, },
}) })
} }
const handleArchive = () => {
if (chore.isActive) {
ArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = { ...chore, isActive: false }
onChoreUpdate(newChore, 'archive')
})
}
})
} else {
UnArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = { ...chore, isActive: true }
onChoreUpdate(newChore, 'unarchive')
})
}
})
}
handleMenuClose()
}
const handleTaskCompletion = () => { const handleTaskCompletion = () => {
setIsPendingCompletion(true) setIsPendingCompletion(true)
@@ -235,10 +149,6 @@ const ChoreCard = ({
} }
const handleChangeDueDate = newDate => { const handleChangeDueDate = newDate => {
if (activeUserId === null) {
alert('Please select a performer')
return
}
UpdateDueDate(chore.id, newDate).then(response => { UpdateDueDate(chore.id, newDate).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
@@ -250,11 +160,6 @@ const ChoreCard = ({
} }
const handleCompleteWithPastDate = newDate => { const handleCompleteWithPastDate = newDate => {
if (activeUserId === null) {
alert('Please select a performer')
return
}
MarkChoreComplete( MarkChoreComplete(
chore.id, chore.id,
impersonatedUser ? { completedBy: impersonatedUser.userId } : null, impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
@@ -318,29 +223,6 @@ const ChoreCard = ({
return 'neutral' return 'neutral'
} }
const getIconForLabel = label => {
if (!label || label.trim() === '') return <></>
switch (String(label).toLowerCase()) {
case 'high':
return <KeyboardDoubleArrowUp />
case 'important':
return <Report />
default:
return <LocalOffer />
}
}
const getPriorityIcon = priority => {
switch (Number(priority)) {
case 1:
return <PriorityHigh />
case 2:
return <KeyboardDoubleArrowUp />
case 3:
return <KeyboardControlKey />
default:
return <HorizontalRule />
}
}
const getRecurrentChipText = chore => { const getRecurrentChipText = chore => {
// if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata: // if chore.frequencyMetadata is type string then parse it otherwise assigned to the metadata:
const metadata = const metadata =
@@ -678,135 +560,19 @@ const ChoreCard = ({
)} )}
</div> </div>
</IconButton> </IconButton>
<IconButton <ChoreActionMenu
// sx={{ width: 15 }} chore={chore}
variant='soft' onChoreUpdate={onChoreUpdate}
color='success' onChoreRemove={onChoreRemove}
onClick={handleMenuOpen} onCompleteWithNote={() => setIsCompleteWithNoteModalOpen(true)}
sx={{ onCompleteWithPastDate={() =>
borderRadius: '50%', setIsCompleteWithPastDateModalOpen(true)
width: 25, }
height: 25, onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
position: 'relative', onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
left: -10, onWriteNFC={() => setIsNFCModalOpen(true)}
}} onDelete={handleDelete}
> />
<MoreVert />
</IconButton>
{/* </ButtonGroup> */}
<Menu
size='lg'
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem
onClick={() => {
setIsCompleteWithNoteModalOpen(true)
}}
>
<NoteAdd />
Complete with note
</MenuItem>
<MenuItem
onClick={() => {
setIsCompleteWithPastDateModalOpen(true)
}}
>
<Update />
Complete in past
</MenuItem>
<MenuItem
onClick={() => {
SkipChore(chore.id)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
onChoreUpdate(newChore, 'skipped')
handleMenuClose()
})
}
})
.catch(error => {
if (error?.queued) {
showError({
title: 'Failed to update',
message:
'Request will be processed when you are online',
})
} else {
showError({
title: 'Failed to update',
message: error,
})
}
})
}}
>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
<MenuItem
onClick={() => {
setIsChangeAssigneeModalOpen(true)
}}
>
<RecordVoiceOver />
Delegate to someone else
</MenuItem>
<Divider />
<MenuItem
onClick={() => {
navigate(`/chores/${chore.id}/history`)
}}
>
<ManageSearch />
History
</MenuItem>
<Divider />
<MenuItem
onClick={() => {
setIsChangeDueDateModalOpen(true)
}}
>
<MoreTime />
Change due date
</MenuItem>
<MenuItem
onClick={() => {
// write current chore URL to NFC
// writeToNFC(`${window.location.origin}/chores/${chore.id}`)
setIsNFCModalOpen(true)
}}
>
<Nfc />
Write to NFC
</MenuItem>
<MenuItem onClick={handleEdit}>
<Edit />
Edit
</MenuItem>
<MenuItem onClick={handleClone}>
<CopyAll />
Clone
</MenuItem>
<MenuItem onClick={handleView}>
<ViewCarousel />
View
</MenuItem>
<MenuItem onClick={handleArchive} color='neutral'>
{chore.isActive ? <Archive /> : <Unarchive />}
{chore.isActive ? 'Archive' : 'Unarchive'}
</MenuItem>
<Divider />
<MenuItem onClick={handleDelete} color='danger'>
<Delete />
Delete
</MenuItem>
</Menu>
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,22 +1,8 @@
import { import {
Archive,
CancelScheduleSend, CancelScheduleSend,
Check, Check,
CopyAll,
Delete,
Edit,
ManageSearch,
MoreTime,
MoreVert,
Nfc,
NoteAdd,
RecordVoiceOver,
Repeat, Repeat,
SwitchAccessShortcut,
TimesOneMobiledata, TimesOneMobiledata,
Unarchive,
Update,
ViewCarousel,
Webhook, Webhook,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@@ -24,15 +10,12 @@ import {
Button, Button,
Chip, Chip,
CircularProgress, CircularProgress,
Divider,
IconButton, IconButton,
Menu,
MenuItem,
Snackbar, Snackbar,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy'
import moment from 'moment' import moment from 'moment'
import React, { useEffect } from 'react' import React from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
@@ -40,11 +23,8 @@ import { useError } from '../../service/ErrorProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx' import { notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx' import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import { import {
ArchiveChore,
DeleteChore, DeleteChore,
MarkChoreComplete, MarkChoreComplete,
SkipChore,
UnArchiveChore,
UpdateChoreAssignee, UpdateChoreAssignee,
UpdateDueDate, UpdateDueDate,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
@@ -54,6 +34,7 @@ import DateModal from '../Modals/Inputs/DateModal'
import SelectModal from '../Modals/Inputs/SelectModal' import SelectModal from '../Modals/Inputs/SelectModal'
import TextModal from '../Modals/Inputs/TextModal' import TextModal from '../Modals/Inputs/TextModal'
import WriteNFCModal from '../Modals/Inputs/WriteNFCModal' import WriteNFCModal from '../Modals/Inputs/WriteNFCModal'
import ChoreActionMenu from '../components/ChoreActionMenu'
const CompactChoreCard = ({ const CompactChoreCard = ({
chore, chore,
@@ -64,7 +45,6 @@ const CompactChoreCard = ({
viewOnly, viewOnly,
onChipClick, onChipClick,
}) => { }) => {
const [activeUserId, setActiveUserId] = React.useState(0)
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] = const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
React.useState(false) React.useState(false)
const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] = const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
@@ -75,10 +55,7 @@ const CompactChoreCard = ({
React.useState(false) React.useState(false)
const [confirmModelConfig, setConfirmModelConfig] = React.useState({}) const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false) const [isNFCModalOpen, setIsNFCModalOpen] = React.useState(false)
const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null)
const navigate = useNavigate() const navigate = useNavigate()
const [isDisabled, setIsDisabled] = React.useState(false)
const [isPendingCompletion, setIsPendingCompletion] = React.useState(false) const [isPendingCompletion, setIsPendingCompletion] = React.useState(false)
const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null) const [secondsLeftToCancel, setSecondsLeftToCancel] = React.useState(null)
@@ -88,44 +65,7 @@ const CompactChoreCard = ({
const { showError } = useError() const { showError } = useError()
useEffect(() => {
document.addEventListener('mousedown', handleMenuOutsideClick)
return () => {
document.removeEventListener('mousedown', handleMenuOutsideClick)
}
}, [anchorEl])
const handleMenuOpen = event => {
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
}
const handleMenuOutsideClick = event => {
if (
anchorEl &&
!anchorEl.contains(event.target) &&
!menuRef.current.contains(event.target)
) {
handleMenuClose()
}
}
// All the existing handler methods (same as original ChoreCard) // All the existing handler methods (same as original ChoreCard)
const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`)
}
const handleClone = () => {
navigate(`/chores/${chore.id}/edit?clone=true`)
}
const handleView = () => {
navigate(`/chores/${chore.id}`)
}
const handleDelete = () => { const handleDelete = () => {
setConfirmModelConfig({ setConfirmModelConfig({
isOpen: true, isOpen: true,
@@ -146,29 +86,6 @@ const CompactChoreCard = ({
}) })
} }
const handleArchive = () => {
if (chore.isActive) {
ArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = { ...chore, isActive: false }
onChoreUpdate(newChore, 'archive')
})
}
})
} else {
UnArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = { ...chore, isActive: true }
onChoreUpdate(newChore, 'unarchive')
})
}
})
}
handleMenuClose()
}
const handleTaskCompletion = () => { const handleTaskCompletion = () => {
setIsPendingCompletion(true) setIsPendingCompletion(true)
let seconds = 3 let seconds = 3
@@ -230,10 +147,6 @@ const CompactChoreCard = ({
} }
const handleChangeDueDate = newDate => { const handleChangeDueDate = newDate => {
if (activeUserId === null) {
alert('Please select a performer')
return
}
UpdateDueDate(chore.id, newDate).then(response => { UpdateDueDate(chore.id, newDate).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
@@ -245,11 +158,6 @@ const CompactChoreCard = ({
} }
const handleCompleteWithPastDate = newDate => { const handleCompleteWithPastDate = newDate => {
if (activeUserId === null) {
alert('Please select a performer')
return
}
MarkChoreComplete( MarkChoreComplete(
chore.id, chore.id,
impersonatedUser ? { completedBy: impersonatedUser.userId } : null, impersonatedUser ? { completedBy: impersonatedUser.userId } : null,
@@ -654,113 +562,30 @@ const CompactChoreCard = ({
)} )}
</IconButton> </IconButton>
{/* Menu Button */} {/* Chore Action Menu */}
<IconButton <ChoreActionMenu
variant='plain' variant='plain'
color='neutral' chore={chore}
size='sm' onChoreUpdate={onChoreUpdate}
onClick={e => { onChoreRemove={onChoreRemove}
e.stopPropagation() onCompleteWithNote={() => setIsCompleteWithNoteModalOpen(true)}
handleMenuOpen(e) onCompleteWithPastDate={() =>
}} setIsCompleteWithPastDateModalOpen(true)
}
onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
onWriteNFC={() => setIsNFCModalOpen(true)}
onDelete={handleDelete}
sx={{ sx={{
width: 28, width: 28,
height: 28, height: 28,
opacity: 0.6, // opacity: 0.6,
'&:hover': { '&:hover': {
opacity: 1, opacity: 1,
}, },
}} }}
> />
<MoreVert sx={{ fontSize: 14 }} />
</IconButton>
</Box> </Box>
{/* Menu */}
<Menu
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={() => setIsCompleteWithNoteModalOpen(true)}>
<NoteAdd />
Complete with note
</MenuItem>
<MenuItem onClick={() => setIsCompleteWithPastDateModalOpen(true)}>
<Update />
Complete in past
</MenuItem>
<MenuItem
onClick={() => {
SkipChore(chore.id)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
onChoreUpdate(newChore, 'skipped')
handleMenuClose()
})
}
})
.catch(error => {
if (error?.queued) {
showError({
title: 'Failed to update',
message: 'Request will be processed when you are online',
})
} else {
showError({
title: 'Failed to update',
message: error,
})
}
})
}}
>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
<MenuItem onClick={() => setIsChangeAssigneeModalOpen(true)}>
<RecordVoiceOver />
Delegate to someone else
</MenuItem>
<Divider />
<MenuItem onClick={() => navigate(`/chores/${chore.id}/history`)}>
<ManageSearch />
History
</MenuItem>
<Divider />
<MenuItem onClick={() => setIsChangeDueDateModalOpen(true)}>
<MoreTime />
Change due date
</MenuItem>
<MenuItem onClick={() => setIsNFCModalOpen(true)}>
<Nfc />
Write to NFC
</MenuItem>
<MenuItem onClick={handleEdit}>
<Edit />
Edit
</MenuItem>
<MenuItem onClick={handleClone}>
<CopyAll />
Clone
</MenuItem>
<MenuItem onClick={handleView}>
<ViewCarousel />
View
</MenuItem>
<MenuItem onClick={handleArchive} color='neutral'>
{chore.isActive ? <Archive /> : <Unarchive />}
{chore.isActive ? 'Archive' : 'Unarchive'}
</MenuItem>
<Divider />
<MenuItem onClick={handleDelete} color='danger'>
<Delete />
Delete
</MenuItem>
</Menu>
</Box> </Box>
{/* All modals (same as original) */} {/* All modals (same as original) */}

View File

@@ -1,4 +1,4 @@
import { CheckCircle, Key, Security, Smartphone } from '@mui/icons-material' import { CheckCircle, Security, Smartphone } from '@mui/icons-material'
import { import {
Alert, Alert,
Box, Box,
@@ -264,7 +264,7 @@ const MFASettings = () => {
</Box> </Box>
</Box> </Box>
</Card> </Card>
{/*
{mfaEnabled && ( {mfaEnabled && (
<Card variant='outlined'> <Card variant='outlined'>
<Box className='flex items-center justify-between'> <Box className='flex items-center justify-between'>
@@ -288,7 +288,7 @@ const MFASettings = () => {
</Button> </Button>
</Box> </Box>
</Card> </Card>
)} )} */}
{/* Setup MFA Modal */} {/* Setup MFA Modal */}
<Modal open={setupModalOpen} onClose={closeSetupModal}> <Modal open={setupModalOpen} onClose={closeSetupModal}>

View File

@@ -122,7 +122,18 @@ const CalendarView = ({ chores }) => {
} }
/> />
{!selectedDate && ( {!selectedDate && (
<Grid container ml={-3} mt={1}> <Grid
container
ml={3}
mt={1}
// start from left:
sx={{
width: '100%',
display: 'flex',
// alignItems: 'center',
justifyContent: 'start',
}}
>
{/* Show legend with current user first, then other circle members who have assignments */} {/* Show legend with current user first, then other circle members who have assignments */}
{(() => { {(() => {
const assignedUserIds = new Set( const assignedUserIds = new Set(
@@ -171,7 +182,7 @@ const CalendarView = ({ chores }) => {
backgroundColor: item.color, backgroundColor: item.color,
}} }}
/> />
<Typography level='body-xs' ml={0.3}> <Typography level='body-xs' ml={0.5}>
{item.name} {item.name}
</Typography> </Typography>
</Grid> </Grid>

View File

@@ -0,0 +1,303 @@
import {
Archive,
CopyAll,
Delete,
Edit,
ManageSearch,
MoreTime,
MoreVert,
Nfc,
NoteAdd,
RecordVoiceOver,
SwitchAccessShortcut,
Unarchive,
Update,
ViewCarousel,
} from '@mui/icons-material'
import { Divider, IconButton, Menu, MenuItem } from '@mui/joy'
import React, { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useError } from '../../service/ErrorProvider'
import {
ArchiveChore,
DeleteChore,
SkipChore,
UnArchiveChore,
} from '../../utils/Fetcher'
const ChoreActionMenu = ({
chore,
onChoreUpdate,
onChoreRemove,
onCompleteWithNote,
onCompleteWithPastDate,
onChangeAssignee,
onChangeDueDate,
onWriteNFC,
onDelete,
sx = {},
variant = 'soft',
}) => {
const [anchorEl, setAnchorEl] = React.useState(null)
const menuRef = React.useRef(null)
const navigate = useNavigate()
const { showError } = useError()
useEffect(() => {
const handleMenuOutsideClick = event => {
if (
anchorEl &&
!anchorEl.contains(event.target) &&
!menuRef.current.contains(event.target)
) {
handleMenuClose()
}
}
document.addEventListener('mousedown', handleMenuOutsideClick)
return () => {
document.removeEventListener('mousedown', handleMenuOutsideClick)
}
}, [anchorEl])
const handleMenuOpen = event => {
event.stopPropagation()
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
}
const handleEdit = () => {
navigate(`/chores/${chore.id}/edit`)
handleMenuClose()
}
const handleClone = () => {
navigate(`/chores/${chore.id}/edit?clone=true`)
handleMenuClose()
}
const handleView = () => {
navigate(`/chores/${chore.id}`)
handleMenuClose()
}
const handleDelete = () => {
if (onDelete) {
onDelete()
} else {
// Default delete behavior
DeleteChore(chore.id).then(response => {
if (response.ok) {
onChoreRemove?.(chore)
}
})
}
handleMenuClose()
}
const handleArchive = () => {
if (chore.isActive) {
ArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(() => {
const newChore = { ...chore, isActive: false }
onChoreUpdate?.(newChore, 'archive')
})
}
})
} else {
UnArchiveChore(chore.id).then(response => {
if (response.ok) {
response.json().then(() => {
const newChore = { ...chore, isActive: true }
onChoreUpdate?.(newChore, 'unarchive')
})
}
})
}
handleMenuClose()
}
const handleSkip = () => {
SkipChore(chore.id)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
onChoreUpdate?.(newChore, 'skipped')
handleMenuClose()
})
}
})
.catch(error => {
if (error?.queued) {
showError({
title: 'Failed to update',
message: 'Request will be processed when you are online',
})
} else {
showError({
title: 'Failed to update',
message: error,
})
}
})
}
const handleHistory = () => {
navigate(`/chores/${chore.id}/history`)
handleMenuClose()
}
return (
<>
<IconButton
variant={variant}
color='success'
onClick={handleMenuOpen}
sx={{
borderRadius: '50%',
width: 25,
height: 25,
position: 'relative',
left: -10,
...sx,
}}
>
<MoreVert />
</IconButton>
<Menu
size='lg'
ref={menuRef}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem
onClick={e => {
e.stopPropagation()
onCompleteWithNote?.()
handleMenuClose()
}}
>
<NoteAdd />
Complete with note
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onCompleteWithPastDate?.()
handleMenuClose()
}}
>
<Update />
Complete in past
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleSkip()
}}
>
<SwitchAccessShortcut />
Skip to next due date
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onChangeAssignee?.()
handleMenuClose()
}}
>
<RecordVoiceOver />
Delegate to someone else
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
handleHistory()
}}
>
<ManageSearch />
History
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
onChangeDueDate?.()
handleMenuClose()
}}
>
<MoreTime />
Change due date
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
onWriteNFC?.()
handleMenuClose()
}}
>
<Nfc />
Write to NFC
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleEdit()
}}
>
<Edit />
Edit
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleClone()
}}
>
<CopyAll />
Clone
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleView()
}}
>
<ViewCarousel />
View
</MenuItem>
<MenuItem
onClick={e => {
e.stopPropagation()
handleArchive()
}}
color='neutral'
>
{chore.isActive ? <Archive /> : <Unarchive />}
{chore.isActive ? 'Archive' : 'Unarchive'}
</MenuItem>
<Divider />
<MenuItem
onClick={e => {
e.stopPropagation()
handleDelete()
}}
color='danger'
>
<Delete />
Delete
</MenuItem>
</Menu>
</>
)
}
export default ChoreActionMenu