aboutsummaryrefslogtreecommitdiffstats
path: root/src/views/Chores
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/views/Chores/ChoreCard.jsx578
-rw-r--r--src/views/Chores/MyChores.jsx384
-rw-r--r--src/views/ChoresOverview.jsx354
3 files changed, 1316 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
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
new file mode 100644
index 0000000..98fd443
--- /dev/null
+++ b/src/views/Chores/MyChores.jsx
@@ -0,0 +1,384 @@
+import { Add, EditCalendar } from '@mui/icons-material'
+import {
+ Badge,
+ Box,
+ Checkbox,
+ CircularProgress,
+ Container,
+ IconButton,
+ List,
+ ListItem,
+ Menu,
+ MenuItem,
+ Snackbar,
+ Typography,
+} from '@mui/joy'
+import { useContext, useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { UserContext } from '../../contexts/UserContext'
+import Logo from '../../Logo'
+import { GetAllUsers, GetChores, GetUserProfile } from '../../utils/Fetcher'
+import ChoreCard from './ChoreCard'
+
+const MyChores = () => {
+ const { userProfile, setUserProfile } = useContext(UserContext)
+ const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
+ const [snackBarMessage, setSnackBarMessage] = useState(null)
+ const [chores, setChores] = useState([])
+ const [filteredChores, setFilteredChores] = useState([])
+ const [selectedFilter, setSelectedFilter] = useState('All')
+ const [activeUserId, setActiveUserId] = useState(0)
+ const [performers, setPerformers] = useState([])
+ const [anchorEl, setAnchorEl] = useState(null)
+ const menuRef = useRef(null)
+ const Navigate = useNavigate()
+ const choreSorter = (a, b) => {
+ // 1. Handle null due dates (always last):
+ if (!a.nextDueDate && !b.nextDueDate) return 0 // Both null, no order
+ if (!a.nextDueDate) return 1 // a is null, comes later
+ if (!b.nextDueDate) return -1 // b is null, comes earlier
+
+ const aDueDate = new Date(a.nextDueDate)
+ const bDueDate = new Date(b.nextDueDate)
+ const now = new Date()
+
+ const oneDayInMs = 24 * 60 * 60 * 1000
+
+ // 2. Prioritize tasks due today +- 1 day:
+ const aTodayOrNear = Math.abs(aDueDate - now) <= oneDayInMs
+ const bTodayOrNear = Math.abs(bDueDate - now) <= oneDayInMs
+ if (aTodayOrNear && !bTodayOrNear) return -1 // a is closer
+ if (!aTodayOrNear && bTodayOrNear) return 1 // b is closer
+
+ // 3. Handle overdue tasks (excluding today +- 1):
+ const aOverdue = aDueDate < now && !aTodayOrNear
+ const bOverdue = bDueDate < now && !bTodayOrNear
+ if (aOverdue && !bOverdue) return -1 // a is overdue, comes earlier
+ if (!aOverdue && bOverdue) return 1 // b is overdue, comes earlier
+
+ // 4. Sort future tasks by due date:
+ return aDueDate - bDueDate // Sort ascending by due date
+ }
+
+ const handleSelectedFilter = selected => {
+ setFilteredChores(FILTERS[selected](chores))
+
+ setSelectedFilter(selected)
+ }
+
+ useEffect(() => {
+ if (userProfile === null) {
+ GetUserProfile()
+ .then(response => response.json())
+ .then(data => {
+ setUserProfile(data.res)
+ })
+ }
+ GetChores()
+ .then(response => response.json())
+ .then(data => {
+ data.res.sort(choreSorter)
+ setChores(data.res)
+
+ setFilteredChores(data.res)
+ })
+
+ GetAllUsers()
+ .then(response => response.json())
+ .then(data => {
+ setPerformers(data.res)
+ })
+
+ const currentUser = JSON.parse(localStorage.getItem('user'))
+ if (currentUser !== null) {
+ setActiveUserId(currentUser.id)
+ }
+ }, [])
+ useEffect(() => {
+ document.addEventListener('mousedown', handleMenuOutsideClick)
+ return () => {
+ document.removeEventListener('mousedown', handleMenuOutsideClick)
+ }
+ }, [anchorEl])
+ const handleMenuOutsideClick = event => {
+ if (
+ anchorEl &&
+ !anchorEl.contains(event.target) &&
+ !menuRef.current.contains(event.target)
+ ) {
+ handleFilterMenuClose()
+ }
+ }
+ const handleFilterMenuOpen = event => {
+ event.preventDefault()
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleFilterMenuClose = () => {
+ setAnchorEl(null)
+ }
+ const handleChoreUpdated = (updatedChore, event) => {
+ const newChores = chores.map(chore => {
+ if (chore.id === updatedChore.id) {
+ return updatedChore
+ }
+ return chore
+ })
+
+ const newFilteredChores = filteredChores.map(chore => {
+ if (chore.id === updatedChore.id) {
+ return updatedChore
+ }
+ return chore
+ })
+ setChores(newChores)
+ setFilteredChores(newFilteredChores)
+ switch (event) {
+ case 'completed':
+ setSnackBarMessage('Completed')
+ break
+ case 'skipped':
+ setSnackBarMessage('Skipped')
+ break
+ case 'rescheduled':
+ setSnackBarMessage('Rescheduled')
+ break
+ default:
+ setSnackBarMessage('Updated')
+ }
+ setIsSnackbarOpen(true)
+ }
+
+ const handleChoreDeleted = deletedChore => {
+ const newChores = chores.filter(chore => chore.id !== deletedChore.id)
+ const newFilteredChores = filteredChores.filter(
+ chore => chore.id !== deletedChore.id,
+ )
+ setChores(newChores)
+ setFilteredChores(newFilteredChores)
+ }
+
+ if (userProfile === null) {
+ return (
+ <Container className='flex h-full items-center justify-center'>
+ <Box className='flex flex-col items-center justify-center'>
+ <CircularProgress
+ color='success'
+ sx={{ '--CircularProgress-size': '200px' }}
+ >
+ <Logo />
+ </CircularProgress>
+ </Box>
+ </Container>
+ )
+ }
+
+ return (
+ <Container maxWidth='md'>
+ {/* <Typography level='h3' mb={1.5}>
+ My Chores
+ </Typography> */}
+ {/* <Sheet> */}
+ <List
+ orientation='horizontal'
+ wrap
+ sx={{
+ '--List-gap': '8px',
+ '--ListItem-radius': '20px',
+ '--ListItem-minHeight': '32px',
+ '--ListItem-gap': '4px',
+ mt: 0.2,
+ }}
+ >
+ {['All', 'Overdue', 'Due today', 'Due in week'].map(filter => (
+ <Badge
+ key={filter}
+ anchorOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
+ variant='outlined'
+ color={selectedFilter === filter ? 'primary' : 'neutral'}
+ badgeContent={FILTERS[filter](chores).length}
+ badgeInset={'5px'}
+ >
+ <ListItem key={filter}>
+ <Checkbox
+ key={'checkbox' + filter}
+ label={filter}
+ onClick={() => handleSelectedFilter(filter)}
+ checked={filter === selectedFilter}
+ disableIcon
+ overlay
+ size='sm'
+ />
+ </ListItem>
+ </Badge>
+ ))}
+
+ <ListItem onClick={handleFilterMenuOpen}>
+ <Checkbox key='checkboxAll' label='⋮' disableIcon overlay size='lg' />
+ </ListItem>
+ <Menu
+ ref={menuRef}
+ anchorEl={anchorEl}
+ open={Boolean(anchorEl)}
+ onClose={handleFilterMenuClose}
+ >
+ <MenuItem
+ onClick={() => {
+ setFilteredChores(
+ FILTERS['Assigned To Me'](chores, userProfile.id),
+ )
+ setSelectedFilter('Assigned To Me')
+ handleFilterMenuClose()
+ }}
+ >
+ Assigned to me
+ </MenuItem>
+ <MenuItem
+ onClick={() => {
+ setFilteredChores(
+ FILTERS['Created By Me'](chores, userProfile.id),
+ )
+ setSelectedFilter('Created By Me')
+ handleFilterMenuClose()
+ }}
+ >
+ Created by me
+ </MenuItem>
+ <MenuItem
+ onClick={() => {
+ setFilteredChores(FILTERS['No Due Date'](chores, userProfile.id))
+ setSelectedFilter('No Due Date')
+ handleFilterMenuClose()
+ }}
+ >
+ No Due Date
+ </MenuItem>
+ </Menu>
+ </List>
+ {/* </Sheet> */}
+ {filteredChores.length === 0 && (
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flexDirection: 'column',
+ height: '50vh',
+ }}
+ >
+ <EditCalendar
+ sx={{
+ fontSize: '4rem',
+ // color: 'text.disabled',
+ mb: 1,
+ }}
+ />
+ <Typography level='title-md' gutterBottom>
+ Nothing scheduled
+ </Typography>
+ </Box>
+ )}
+
+ {filteredChores.map(chore => (
+ <ChoreCard
+ key={chore.id}
+ chore={chore}
+ onChoreUpdate={handleChoreUpdated}
+ onChoreRemove={handleChoreDeleted}
+ performers={performers}
+ />
+ ))}
+
+ <Box
+ // variant='outlined'
+ sx={{
+ position: 'fixed',
+ bottom: 0,
+ left: 10,
+ p: 2, // padding
+ display: 'flex',
+ justifyContent: 'flex-end',
+ gap: 2,
+ 'z-index': 1000,
+ }}
+ >
+ <IconButton
+ color='primary'
+ variant='solid'
+ sx={{
+ borderRadius: '50%',
+ width: 50,
+ height: 50,
+ }}
+ // startDecorator={<Add />}
+ onClick={() => {
+ Navigate(`/chores/create`)
+ }}
+ >
+ <Add />
+ </IconButton>
+ </Box>
+ <Snackbar
+ open={isSnackbarOpen}
+ onClose={() => {
+ setIsSnackbarOpen(false)
+ }}
+ autoHideDuration={3000}
+ variant='soft'
+ color='success'
+ size='lg'
+ invertedColors
+ >
+ <Typography level='title-md'>{snackBarMessage}</Typography>
+ </Snackbar>
+ </Container>
+ )
+}
+
+const FILTERS = {
+ All: function (chores) {
+ return chores
+ },
+ Overdue: function (chores) {
+ return chores.filter(chore => {
+ if (chore.nextDueDate === null) return false
+ return new Date(chore.nextDueDate) < new Date()
+ })
+ },
+ 'Due today': function (chores) {
+ return chores.filter(chore => {
+ return (
+ new Date(chore.nextDueDate).toDateString() === new Date().toDateString()
+ )
+ })
+ },
+ 'Due in week': function (chores) {
+ return chores.filter(chore => {
+ return (
+ new Date(chore.nextDueDate) <
+ new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) &&
+ new Date(chore.nextDueDate) > new Date()
+ )
+ })
+ },
+ 'Created By Me': function (chores, userID) {
+ return chores.filter(chore => {
+ return chore.createdBy === userID
+ })
+ },
+ 'Assigned To Me': function (chores, userID) {
+ return chores.filter(chore => {
+ return chore.assignedTo === userID
+ })
+ },
+ 'No Due Date': function (chores, userID) {
+ return chores.filter(chore => {
+ return chore.nextDueDate === null
+ })
+ },
+}
+
+export default MyChores
diff --git a/src/views/ChoresOverview.jsx b/src/views/ChoresOverview.jsx
new file mode 100644
index 0000000..396ab0d
--- /dev/null
+++ b/src/views/ChoresOverview.jsx
@@ -0,0 +1,354 @@
+import {
+ Adjust,
+ CancelRounded,
+ CheckBox,
+ Edit,
+ HelpOutline,
+ History,
+ QueryBuilder,
+ SearchRounded,
+ Warning,
+} from '@mui/icons-material'
+import {
+ Avatar,
+ Button,
+ ButtonGroup,
+ Chip,
+ Container,
+ Grid,
+ IconButton,
+ Input,
+ Table,
+ Tooltip,
+ Typography,
+} from '@mui/joy'
+
+import moment from 'moment'
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { API_URL } from '../Config'
+import { GetAllUsers } from '../utils/Fetcher'
+import { Fetch } from '../utils/TokenManager'
+import DateModal from './Modals/Inputs/DateModal'
+// import moment from 'moment'
+
+// enum for chore status:
+const CHORE_STATUS = {
+ NO_DUE_DATE: 'No due date',
+ DUE_SOON: 'Soon',
+ DUE_NOW: 'Due',
+ OVER_DUE: 'Overdue',
+}
+
+const ChoresOverview = () => {
+ const [chores, setChores] = useState([])
+ const [filteredChores, setFilteredChores] = useState([])
+ const [performers, setPerformers] = useState([])
+ const [activeUserId, setActiveUserId] = useState(null)
+ const [isDateModalOpen, setIsDateModalOpen] = useState(false)
+ const [choreId, setChoreId] = useState(null)
+ const [search, setSearch] = useState('')
+ const Navigate = useNavigate()
+
+ const getChoreStatus = chore => {
+ if (chore.nextDueDate === null) {
+ return CHORE_STATUS.NO_DUE_DATE
+ }
+ const dueDate = new Date(chore.nextDueDate)
+ const now = new Date()
+ const diff = dueDate - now
+ if (diff < 0) {
+ return CHORE_STATUS.OVER_DUE
+ }
+ if (diff > 1000 * 60 * 60 * 24) {
+ return CHORE_STATUS.DUE_NOW
+ }
+ if (diff > 0) {
+ return CHORE_STATUS.DUE_SOON
+ }
+ return CHORE_STATUS.NO_DUE_DATE
+ }
+ const getChoreStatusColor = chore => {
+ switch (getChoreStatus(chore)) {
+ case CHORE_STATUS.NO_DUE_DATE:
+ return 'neutral'
+ case CHORE_STATUS.DUE_SOON:
+ return 'success'
+ case CHORE_STATUS.DUE_NOW:
+ return 'primary'
+ case CHORE_STATUS.OVER_DUE:
+ return 'warning'
+ default:
+ return 'neutral'
+ }
+ }
+ const getChoreStatusIcon = chore => {
+ switch (getChoreStatus(chore)) {
+ case CHORE_STATUS.NO_DUE_DATE:
+ return <HelpOutline />
+ case CHORE_STATUS.DUE_SOON:
+ return <QueryBuilder />
+ case CHORE_STATUS.DUE_NOW:
+ return <Adjust />
+ case CHORE_STATUS.OVER_DUE:
+ return <Warning />
+ default:
+ return <HelpOutline />
+ }
+ }
+ useEffect(() => {
+ // fetch chores:
+ Fetch(`${API_URL}/chores/`)
+ .then(response => response.json())
+ .then(data => {
+ const filteredData = data.res.filter(
+ chore => chore.assignedTo === activeUserId || chore.assignedTo === 0,
+ )
+ setChores(data.res)
+ setFilteredChores(data.res)
+ })
+ GetAllUsers()
+ .then(response => response.json())
+ .then(data => {
+ setPerformers(data.res)
+ })
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (user != null && user.id > 0) {
+ setActiveUserId(user.id)
+ }
+ }, [])
+
+ return (
+ <Container>
+ <Typography level='h4' mb={1.5}>
+ Chores Overviews
+ </Typography>
+ {/* <SummaryCard /> */}
+ <Grid container>
+ <Grid
+ item
+ sm={6}
+ alignSelf={'flex-start'}
+ minWidth={100}
+ display='flex'
+ gap={2}
+ >
+ <Input
+ placeholder='Search'
+ value={search}
+ onChange={e => {
+ if (e.target.value === '') {
+ setFilteredChores(chores)
+ }
+ setSearch(e.target.value)
+ const newChores = chores.filter(chore => {
+ return chore.name.includes(e.target.value)
+ })
+ setFilteredChores(newChores)
+ }}
+ endDecorator={
+ search !== '' ? (
+ <Button
+ variant='text'
+ onClick={() => {
+ setSearch('')
+ setFilteredChores(chores)
+ }}
+ >
+ <CancelRounded />
+ </Button>
+ ) : (
+ <Button variant='text'>
+ <SearchRounded />
+ </Button>
+ )
+ }
+ ></Input>
+ </Grid>
+ <Grid item sm={6} justifyContent={'flex-end'} display={'flex'} gap={2}>
+ <Button
+ onClick={() => {
+ Navigate(`/chores/create`)
+ }}
+ >
+ New Chore
+ </Button>
+ </Grid>
+ </Grid>
+
+ <Table>
+ <thead>
+ <tr>
+ {/* first column has minium size because its icon */}
+ <th style={{ width: 100 }}>Due</th>
+ <th>Chore</th>
+ <th>Assignee</th>
+ <th>Due</th>
+ <th>Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {filteredChores.map(chore => (
+ <tr key={chore.id}>
+ {/* cirular icon if the chore is due will be red else yellow: */}
+ <td>
+ <Chip color={getChoreStatusColor(chore)}>
+ {getChoreStatus(chore)}
+ </Chip>
+ </td>
+ <td
+ onClick={() => {
+ Navigate(`/chores/${chore.id}/edit`)
+ }}
+ >
+ {chore.name || '--'}
+ </td>
+ <td>
+ {chore.assignedTo > 0 ? (
+ <Tooltip
+ title={
+ performers.find(p => p.id === chore.assignedTo)
+ ?.displayName
+ }
+ size='sm'
+ >
+ <Chip
+ startDecorator={
+ <Avatar color='primary'>
+ {
+ performers.find(p => p.id === chore.assignedTo)
+ ?.displayName[0]
+ }
+ </Avatar>
+ }
+ >
+ {performers.find(p => p.id === chore.assignedTo)?.name}
+ </Chip>
+ </Tooltip>
+ ) : (
+ <Chip
+ color='warning'
+ startDecorator={<Avatar color='primary'>?</Avatar>}
+ >
+ Unassigned
+ </Chip>
+ )}
+ </td>
+ <td>
+ <Tooltip
+ title={
+ chore.nextDueDate === null
+ ? 'no due date'
+ : moment(chore.nextDueDate).format('YYYY-MM-DD')
+ }
+ size='sm'
+ >
+ <Typography>
+ {chore.nextDueDate === null
+ ? '--'
+ : moment(chore.nextDueDate).fromNow()}
+ </Typography>
+ </Tooltip>
+ </td>
+
+ <td>
+ <ButtonGroup
+ // display='flex'
+ // // justifyContent='space-around'
+ // alignItems={'center'}
+ // gap={0.5}
+ >
+ <IconButton
+ variant='outlined'
+ size='sm'
+ // sx={{ borderRadius: '50%' }}
+ onClick={() => {
+ Fetch(`${API_URL}/chores/${chore.id}/do`, {
+ method: 'POST',
+ }).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ const newChores = [...chores]
+ const index = newChores.findIndex(
+ c => c.id === chore.id,
+ )
+ newChores[index] = newChore
+ setChores(newChores)
+ setFilteredChores(newChores)
+ })
+ }
+ })
+ }}
+ aria-setsize={2}
+ >
+ <CheckBox />
+ </IconButton>
+ <IconButton
+ variant='outlined'
+ size='sm'
+ // sx={{ borderRadius: '50%' }}
+ onClick={() => {
+ setChoreId(chore.id)
+ setIsDateModalOpen(true)
+ }}
+ aria-setsize={2}
+ >
+ <History />
+ </IconButton>
+ <IconButton
+ variant='outlined'
+ size='sm'
+ // sx={{
+ // borderRadius: '50%',
+ // }}
+ onClick={() => {
+ Navigate(`/chores/${chore.id}/edit`)
+ }}
+ >
+ <Edit />
+ </IconButton>
+ </ButtonGroup>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </Table>
+ <DateModal
+ isOpen={isDateModalOpen}
+ key={choreId}
+ title={`Change due date`}
+ onClose={() => {
+ setIsDateModalOpen(false)
+ }}
+ onSave={date => {
+ if (activeUserId === null) {
+ alert('Please select a performer')
+ return
+ }
+ fetch(
+ `${API_URL}/chores/${choreId}/do?performer=${activeUserId}&completedDate=${new Date(
+ date,
+ ).toISOString()}`,
+ {
+ method: 'POST',
+ },
+ ).then(response => {
+ if (response.ok) {
+ response.json().then(data => {
+ const newChore = data.res
+ const newChores = [...chores]
+ const index = newChores.findIndex(c => c.id === chore.id)
+ newChores[index] = newChore
+ setChores(newChores)
+ setFilteredChores(newChores)
+ })
+ }
+ })
+ }}
+ />
+ </Container>
+ )
+}
+
+export default ChoresOverview