2024-07-06 03:49:51 -04:00
|
|
|
import { Add, CancelRounded, EditCalendar } from '@mui/icons-material'
|
2024-06-30 18:55:39 -04:00
|
|
|
import {
|
|
|
|
Badge,
|
|
|
|
Box,
|
|
|
|
Checkbox,
|
|
|
|
CircularProgress,
|
|
|
|
Container,
|
|
|
|
IconButton,
|
2024-07-06 03:49:51 -04:00
|
|
|
Input,
|
2024-06-30 18:55:39 -04:00
|
|
|
List,
|
|
|
|
ListItem,
|
|
|
|
Menu,
|
|
|
|
MenuItem,
|
|
|
|
Snackbar,
|
|
|
|
Typography,
|
|
|
|
} from '@mui/joy'
|
2024-07-06 03:49:51 -04:00
|
|
|
import Fuse from 'fuse.js'
|
2024-06-30 18:55:39 -04:00
|
|
|
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')
|
2024-07-06 03:49:51 -04:00
|
|
|
const [searchTerm, setSearchTerm] = useState('')
|
2024-06-30 18:55:39 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-07-06 03:49:51 -04:00
|
|
|
const searchOptions = {
|
|
|
|
// keys to search in
|
|
|
|
keys: ['name', 'labels'],
|
|
|
|
includeScore: true, // Optional: if you want to see how well each result matched the search term
|
|
|
|
isCaseSensitive: false,
|
|
|
|
findAllMatches: true,
|
|
|
|
}
|
|
|
|
const fuse = new Fuse(chores, searchOptions)
|
|
|
|
|
|
|
|
const handleSearchChange = e => {
|
|
|
|
const search = e.target.value
|
|
|
|
if (search === '') {
|
|
|
|
setFilteredChores(chores)
|
|
|
|
setSearchTerm('')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const term = search.toLowerCase()
|
|
|
|
setSearchTerm(term)
|
|
|
|
setFilteredChores(fuse.search(term).map(result => result.item))
|
|
|
|
}
|
|
|
|
|
2024-06-30 18:55:39 -04:00
|
|
|
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>
|
2024-07-06 03:49:51 -04:00
|
|
|
{/* Search box to filter */}
|
|
|
|
<Box>
|
|
|
|
<Input
|
|
|
|
placeholder='Search'
|
|
|
|
value={searchTerm}
|
|
|
|
sx={{
|
|
|
|
mt: 1,
|
|
|
|
mb: 1,
|
|
|
|
borderRadius: 20,
|
|
|
|
// border: '1px solid',
|
|
|
|
borderColor: 'text.disabled',
|
|
|
|
padding: 1,
|
|
|
|
}}
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
endDecorator={
|
|
|
|
<CancelRounded
|
|
|
|
onClick={() => {
|
|
|
|
setSearchTerm('')
|
|
|
|
setFilteredChores(chores)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
|
2024-06-30 18:55:39 -04:00
|
|
|
{/* </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
|