aboutsummaryrefslogtreecommitdiffstats
path: root/src/views/Chores/ChoreCard.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/views/Chores/ChoreCard.jsx')
-rw-r--r--src/views/Chores/ChoreCard.jsx578
1 files changed, 578 insertions, 0 deletions
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
new file mode 100644
index 0000000..0b2a408
--- /dev/null
+++ b/src/views/Chores/ChoreCard.jsx
@@ -0,0 +1,578 @@
+import {
+ Check,
+ Delete,
+ Edit,
+ HowToReg,
+ KeyboardDoubleArrowUp,
+ LocalOffer,
+ ManageSearch,
+ MoreTime,
+ MoreVert,
+ NoteAdd,
+ RecordVoiceOver,
+ Repeat,
+ Report,
+ SwitchAccessShortcut,
+ TimesOneMobiledata,
+ Update,
+ Webhook,
+} from '@mui/icons-material'
+import {
+ Avatar,
+ Box,
+ Card,
+ Chip,
+ CircularProgress,
+ Divider,
+ Grid,
+ IconButton,
+ Menu,
+ MenuItem,
+ Typography,
+} from '@mui/joy'
+import moment from 'moment'
+import React, { useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { API_URL } from '../../Config'
+import { Fetch } from '../../utils/TokenManager'
+import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
+import DateModal from '../Modals/Inputs/DateModal'
+import SelectModal from '../Modals/Inputs/SelectModal'
+import TextModal from '../Modals/Inputs/TextModal'
+const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
+ const [activeUserId, setActiveUserId] = React.useState(0)
+ const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
+ React.useState(false)
+ const [isCompleteWithPastDateModalOpen, setIsCompleteWithPastDateModalOpen] =
+ React.useState(false)
+ const [isChangeAssigneeModalOpen, setIsChangeAssigneeModalOpen] =
+ React.useState(false)
+ const [isCompleteWithNoteModalOpen, setIsCompleteWithNoteModalOpen] =
+ React.useState(false)
+ const [confirmModelConfig, setConfirmModelConfig] = React.useState({})
+ const [anchorEl, setAnchorEl] = React.useState(null)
+ const menuRef = React.useRef(null)
+ const navigate = useNavigate()
+ const [isDisabled, setIsDisabled] = React.useState(false)
+
+ // useEffect(() => {
+ // GetAllUsers()
+ // .then(response => response.json())
+ // .then(data => {
+ // setPerformers(data.res)
+ // })
+ // }, [])
+
+ 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 handleDelete = () => {
+ setConfirmModelConfig({
+ isOpen: true,
+ title: 'Delete Chore',
+ confirmText: 'Delete',
+ cancelText: 'Cancel',
+ message: 'Are you sure you want to delete this chore?',
+ onClose: isConfirmed => {
+ console.log('isConfirmed', isConfirmed)
+ if (isConfirmed === true) {
+ Fetch(`${API_URL}/chores/${chore.id}`, {
+ method: 'DELETE',
+ }).then(response => {
+ if (response.ok) {
+ onChoreRemove(chore)
+ }
+ })
+ }
+ setConfirmModelConfig({})
+ },
+ })
+ }
+
+ const handleCompleteChore = () => {
+ Fetch(`${API_URL}/chores/${chore.id}/do`, {
+ method: 'POST',
+ }).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ onChoreUpdate(newChore, 'completed')
+ })
+ }
+ })
+ setIsDisabled(true)
+ setTimeout(() => setIsDisabled(false), 5000) // Re-enable the button after 5 seconds
+ }
+ const handleChangeDueDate = newDate => {
+ if (activeUserId === null) {
+ alert('Please select a performer')
+ return
+ }
+ Fetch(`${API_URL}/chores/${chore.id}/dueDate`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ dueDate: newDate ? new Date(newDate).toISOString() : null,
+ UpdatedBy: activeUserId,
+ }),
+ }).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ onChoreUpdate(newChore, 'rescheduled')
+ })
+ }
+ })
+ }
+
+ const handleCompleteWithPastDate = newDate => {
+ if (activeUserId === null) {
+ alert('Please select a performer')
+ return
+ }
+ Fetch(
+ `${API_URL}/chores/${chore.id}/do?completedDate=${new Date(
+ newDate,
+ ).toISOString()}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ },
+ ).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ onChoreUpdate(newChore, 'completed')
+ })
+ }
+ })
+ }
+ const handleAssigneChange = assigneeId => {
+ // TODO: Implement assignee change
+ }
+ const handleCompleteWithNote = note => {
+ Fetch(`${API_URL}/chores/${chore.id}/do`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ note: note,
+ }),
+ }).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ onChoreUpdate(newChore, 'completed')
+ })
+ }
+ })
+ }
+ const getDueDateChipText = nextDueDate => {
+ if (chore.nextDueDate === null) return 'No Due Date'
+ // if due in next 48 hours, we should it in this format : Tomorrow 11:00 AM
+ const diff = moment(nextDueDate).diff(moment(), 'hours')
+ if (diff < 48 && diff > 0) {
+ return moment(nextDueDate).calendar().replace(' at', '')
+ }
+ return 'Due ' + moment(nextDueDate).fromNow()
+ }
+ const getDueDateChipColor = nextDueDate => {
+ if (chore.nextDueDate === null) return 'neutral'
+ const diff = moment(nextDueDate).diff(moment(), 'hours')
+ if (diff < 48 && diff > 0) {
+ return 'warning'
+ }
+ if (diff < 0) {
+ return 'danger'
+ }
+
+ 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 getRecurrentChipText = chore => {
+ const dayOfMonthSuffix = n => {
+ if (n >= 11 && n <= 13) {
+ return 'th'
+ }
+ switch (n % 10) {
+ case 1:
+ return 'st'
+ case 2:
+ return 'nd'
+ case 3:
+ return 'rd'
+ default:
+ return 'th'
+ }
+ }
+ if (chore.frequencyType === 'once') {
+ return 'Once'
+ } else if (chore.frequencyType === 'trigger') {
+ return 'Trigger'
+ } else if (chore.frequencyType === 'daily') {
+ return 'Daily'
+ } else if (chore.frequencyType === 'weekly') {
+ return 'Weekly'
+ } else if (chore.frequencyType === 'monthly') {
+ return 'Monthly'
+ } else if (chore.frequencyType === 'yearly') {
+ return 'Yearly'
+ } else if (chore.frequencyType === 'days_of_the_week') {
+ let days = JSON.parse(chore.frequencyMetadata).days
+ days = days.map(d => moment().day(d).format('ddd'))
+ return days.join(', ')
+ } else if (chore.frequencyType === 'day_of_the_month') {
+ let freqData = JSON.parse(chore.frequencyMetadata)
+ const months = freqData.months.map(m => moment().month(m).format('MMM'))
+ return `${chore.frequency}${dayOfMonthSuffix(
+ chore.frequency,
+ )} of ${months.join(', ')}`
+ } else if (chore.frequencyType === 'interval') {
+ return `Every ${chore.frequency} ${
+ JSON.parse(chore.frequencyMetadata).unit
+ }`
+ } else {
+ return chore.frequencyType
+ }
+ }
+
+ const getFrequencyIcon = chore => {
+ if (['once', 'no_repeat'].includes(chore.frequencyType)) {
+ return <TimesOneMobiledata />
+ } else if (chore.frequencyType === 'trigger') {
+ return <Webhook />
+ } else {
+ return <Repeat />
+ }
+ }
+
+ return (
+ <>
+ <Chip
+ variant='soft'
+ sx={{
+ position: 'relative',
+ top: 10,
+ zIndex: 1,
+ left: 10,
+ }}
+ color={getDueDateChipColor(chore.nextDueDate)}
+ >
+ {getDueDateChipText(chore.nextDueDate)}
+ </Chip>
+
+ <Chip
+ variant='soft'
+ sx={{
+ position: 'relative',
+ top: 10,
+ zIndex: 1,
+ ml: 0.4,
+ left: 10,
+ }}
+ >
+ <div
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ >
+ {getFrequencyIcon(chore)}
+
+ {getRecurrentChipText(chore)}
+ </div>
+ </Chip>
+
+ <Card
+ variant='plain'
+ sx={{
+ ...sx,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ p: 2,
+ // backgroundColor: 'white',
+ boxShadow: 'sm',
+ borderRadius: 20,
+
+ // mb: 2,
+ }}
+ >
+ <Grid container>
+ <Grid item xs={9}>
+ {/* Box in top right with Chip showing next due date */}
+ <Box display='flex' justifyContent='start' alignItems='center'>
+ <Avatar sx={{ mr: 1, fontSize: 22 }}>
+ {chore.name.charAt(0).toUpperCase()}
+ </Avatar>
+ <Box display='flex' flexDirection='column'>
+ <Typography level='title-md'>{chore.name}</Typography>
+ <Typography level='body-md' color='text.disabled'>
+ Assigned to{' '}
+ <Chip variant='outlined'>
+ {
+ performers.find(p => p.id === chore.assignedTo)
+ ?.displayName
+ }
+ </Chip>
+ </Typography>
+ <Box>
+ {chore.labels?.split(',').map(label => (
+ <Chip
+ variant='solid'
+ key={label}
+ color='primary'
+ sx={{
+ position: 'relative',
+ ml: 0.5,
+ top: 10,
+ zIndex: 1,
+ left: 10,
+ }}
+ startDecorator={getIconForLabel(label)}
+ >
+ {label}
+ </Chip>
+ ))}
+ </Box>
+ </Box>
+ </Box>
+ {/* <Box display='flex' justifyContent='space-between' alignItems='center'>
+ <Chip variant='outlined'>
+ {chore.nextDueDate === null
+ ? '--'
+ : 'Due ' + moment(chore.nextDueDate).fromNow()}
+ </Chip>
+ </Box> */}
+ </Grid>
+ <Grid
+ item
+ xs={3}
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ }}
+ >
+ <Box display='flex' justifyContent='flex-end' alignItems='flex-end'>
+ {/* <ButtonGroup> */}
+ <IconButton
+ variant='solid'
+ color='success'
+ onClick={handleCompleteChore}
+ disabled={isDisabled}
+ sx={{
+ borderRadius: '50%',
+ width: 50,
+ height: 50,
+ zIndex: 1,
+ }}
+ >
+ <div className='relative grid place-items-center'>
+ <Check />
+ {isDisabled && (
+ <CircularProgress
+ variant='solid'
+ color='success'
+ size='md'
+ sx={{
+ color: 'success.main',
+ position: 'absolute',
+ zIndex: 0,
+ }}
+ />
+ )}
+ </div>
+ </IconButton>
+ <IconButton
+ // sx={{ width: 15 }}
+ variant='soft'
+ color='success'
+ onClick={handleMenuOpen}
+ sx={{
+ borderRadius: '50%',
+ width: 25,
+ height: 25,
+ position: 'relative',
+ left: -10,
+ }}
+ >
+ <MoreVert />
+ </IconButton>
+ {/* </ButtonGroup> */}
+ <Menu
+ size='md'
+ 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={() => {
+ Fetch(`${API_URL}/chores/${chore.id}/skip`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ }).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ onChoreUpdate(newChore, 'skipped')
+ handleMenuClose()
+ })
+ }
+ })
+ }}
+ >
+ <SwitchAccessShortcut />
+ Skip to next due date
+ </MenuItem>
+ <MenuItem
+ onClick={() => {
+ setIsChangeAssigneeModalOpen(true)
+ }}
+ >
+ <RecordVoiceOver />
+ Delegate to someone else
+ </MenuItem>
+ <MenuItem>
+ <HowToReg />
+ Complete as 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={handleEdit}>
+ <Edit />
+ Edit
+ </MenuItem>
+ <MenuItem onClick={handleDelete} color='danger'>
+ <Delete />
+ Delete
+ </MenuItem>
+ </Menu>
+ </Box>
+ </Grid>
+ </Grid>
+ <DateModal
+ isOpen={isChangeDueDateModalOpen}
+ key={'changeDueDate' + chore.id}
+ current={chore.nextDueDate}
+ title={`Change due date`}
+ onClose={() => {
+ setIsChangeDueDateModalOpen(false)
+ }}
+ onSave={handleChangeDueDate}
+ />
+ <DateModal
+ isOpen={isCompleteWithPastDateModalOpen}
+ key={'completedInPast' + chore.id}
+ current={chore.nextDueDate}
+ title={`Save Chore that you completed in the past`}
+ onClose={() => {
+ setIsCompleteWithPastDateModalOpen(false)
+ }}
+ onSave={handleCompleteWithPastDate}
+ />
+ <SelectModal
+ isOpen={isChangeAssigneeModalOpen}
+ options={performers}
+ displayKey='displayName'
+ title={`Delegate to someone else`}
+ onClose={() => {
+ setIsChangeAssigneeModalOpen(false)
+ }}
+ onSave={handleAssigneChange}
+ />
+ <ConfirmationModal config={confirmModelConfig} />
+ <TextModal
+ isOpen={isCompleteWithNoteModalOpen}
+ title='Add note to attach to this completion:'
+ onClose={() => {
+ setIsCompleteWithNoteModalOpen(false)
+ }}
+ okText={'Complete'}
+ onSave={handleCompleteWithNote}
+ />
+ </Card>
+ </>
+ )
+}
+
+export default ChoreCard