import {
Add,
CancelRounded,
EditCalendar,
ExpandCircleDown,
FilterAlt,
PriorityHigh,
Style,
} from '@mui/icons-material'
import {
Accordion,
AccordionDetails,
AccordionGroup,
Box,
Button,
Chip,
Container,
Divider,
IconButton,
Input,
List,
Menu,
MenuItem,
Snackbar,
Typography,
} from '@mui/joy'
import Fuse from 'fuse.js'
import { useContext, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { UserContext } from '../../contexts/UserContext'
import { useChores } from '../../queries/ChoreQueries'
import { GetAllUsers, GetUserProfile } from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import { useLabels } from '../Labels/LabelQueries'
import ChoreCard from './ChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
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 [choreSections, setChoreSections] = useState([])
const [openChoreSections, setOpenChoreSections] = useState({})
const [searchTerm, setSearchTerm] = useState('')
const [activeUserId, setActiveUserId] = useState(0)
const [performers, setPerformers] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const menuRef = useRef(null)
const Navigate = useNavigate()
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
const { data: choresData, isLoading: choresLoading } = useChores()
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 sectionSorter = (t, chores) => {
// sort by priority then due date:
chores.sort((a, b) => {
// no priority is lowest priority:
if (a.priority === 0) {
return 1
}
if (a.priority !== b.priority) {
return a.priority - b.priority
}
if (a.nextDueDate === null) {
return 1
}
if (b.nextDueDate === null) {
return -1
}
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
})
var groups = []
switch (t) {
case 'due_date':
var groupRaw = {
Today: [],
'In a week': [],
'This month': [],
Later: [],
Overdue: [],
Anytime: [],
}
chores.forEach(chore => {
if (chore.nextDueDate === null) {
groupRaw['Anytime'].push(chore)
} else if (new Date(chore.nextDueDate) < new Date()) {
groupRaw['Overdue'].push(chore)
} else if (
new Date(chore.nextDueDate).toDateString() ===
new Date().toDateString()
) {
groupRaw['Today'].push(chore)
} else if (
new Date(chore.nextDueDate) <
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) &&
new Date(chore.nextDueDate) > new Date()
) {
groupRaw['In a week'].push(chore)
} else if (
new Date(chore.nextDueDate).getMonth() === new Date().getMonth()
) {
groupRaw['This month'].push(chore)
} else {
groupRaw['Later'].push(chore)
}
})
groups = [
{ name: 'Overdue', content: groupRaw['Overdue'] },
{ name: 'Today', content: groupRaw['Today'] },
{ name: 'In a week', content: groupRaw['In a week'] },
{ name: 'This month', content: groupRaw['This month'] },
{ name: 'Later', content: groupRaw['Later'] },
{ name: 'Anytime', content: groupRaw['Anytime'] },
]
break
case 'priority':
groupRaw = {
p1: [],
p2: [],
p3: [],
p4: [],
no_priority: [],
}
chores.forEach(chore => {
switch (chore.priority) {
case 1:
groupRaw['p1'].push(chore)
break
case 2:
groupRaw['p2'].push(chore)
break
case 3:
groupRaw['p3'].push(chore)
break
case 4:
groupRaw['p4'].push(chore)
break
}
})
break
case 'labels':
groupRaw = {}
chores.forEach(chore => {
chore.labelsV2.forEach(label => {
if (groupRaw[label.id] === undefined) {
groupRaw[label.id] = []
}
groupRaw[label.id].push(chore)
})
})
}
return groups
}
useEffect(() => {
if (userProfile === null) {
GetUserProfile()
.then(response => response.json())
.then(data => {
setUserProfile(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(() => {
if (choresData) {
const sortedChores = choresData.res.sort(choreSorter)
setChores(sortedChores)
setFilteredChores(sortedChores)
const sections = sectionSorter('due_date', sortedChores)
setChoreSections(sections)
setOpenChoreSections(
Object.keys(sections).reduce((acc, key) => {
acc[key] = true
return acc
}, {}),
)
}
}, [choresData, choresLoading])
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 handleLabelFiltering = chipClicked => {
if (chipClicked.label) {
const label = chipClicked.label
const labelFiltered = [...chores].filter(chore =>
chore.labelsV2.some(
l => l.id === label.id && l.created_by === label.created_by,
),
)
setFilteredChores(labelFiltered)
setSelectedFilter('Label: ' + label.name)
} else if (chipClicked.priority) {
const priority = chipClicked.priority
const priorityFiltered = chores.filter(
chore => chore.priority === priority,
)
setFilteredChores(priorityFiltered)
setSelectedFilter('Priority: ' + priority)
}
}
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)
}
const searchOptions = {
// keys to search in
keys: ['name', 'raw_label'],
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.map(c => ({
...c,
raw_label: c.labelsV2.map(c => c.name).join(' '),
})),
searchOptions,
)
const handleSearchChange = e => {
if (selectedFilter !== 'All') {
setSelectedFilter('All')
}
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))
}
if (
userProfile === null ||
userLabelsLoading ||
performers.length === 0 ||
choresLoading
) {
return
}
return (
{
setSearchTerm('')
setFilteredChores(chores)
}}
/>
)
}
/>
}
options={Priorities}
selectedItem={selectedFilter}
onItemSelect={selected => {
handleLabelFiltering({ priority: selected.value })
}}
mouseClickHandler={handleMenuOutsideClick}
isActive={selectedFilter.startsWith('Priority: ')}
/>
}
// TODO : this need simplification we want to display both user labels and chore labels
// that why we are merging them here.
// we also filter out the labels that user created as those will be part of user labels
options={[
...userLabels,
...chores
.map(c => c.labelsV2)
.flat()
.filter(l => l.created_by !== userProfile.id)
.map(l => {
// if user created it don't show it:
return {
...l,
name: l.name + ' (Shared Label)',
}
}),
]}
selectedItem={selectedFilter}
onItemSelect={selected => {
handleLabelFiltering({ label: selected })
}}
isActive={selectedFilter.startsWith('Label: ')}
mouseClickHandler={handleMenuOutsideClick}
useChips
/>
{selectedFilter !== 'All' && (
{
setFilteredChores(chores)
setSelectedFilter('All')
}}
endDecorator={}
onClick={() => {
setFilteredChores(chores)
setSelectedFilter('All')
}}
>
Current Filter: {selectedFilter}
)}
{filteredChores.length === 0 && (
Nothing scheduled
{chores.length > 0 && (
<>
>
)}
)}
{(searchTerm?.length > 0 || selectedFilter !== 'All') &&
filteredChores.map(chore => (
))}
{searchTerm.length === 0 && selectedFilter === 'All' && (
{choreSections.map((section, index) => {
if (section.content.length === 0) return null
return (
{
if (openChoreSections[index]) {
const newOpenChoreSections = { ...openChoreSections }
delete newOpenChoreSections[index]
setOpenChoreSections(newOpenChoreSections)
} else {
setOpenChoreSections({
...openChoreSections,
[index]: true,
})
}
}}
endDecorator={
openChoreSections[index] ? (
) : (
)
}
startDecorator={
<>
{section?.content?.length}
>
}
>
{section.name}
{section.content?.map(chore => (
))}
)
})}
)}
}
onClick={() => {
Navigate(`/chores/create`)
}}
>
{
setIsSnackbarOpen(false)
}}
autoHideDuration={3000}
variant='soft'
color='success'
size='lg'
invertedColors
>
{snackBarMessage}
)
}
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()
)
})
},
'Due Later': function (chores) {
return chores.filter(chore => {
return (
new Date(chore.nextDueDate) > new Date(Date.now() + 24 * 60 * 60 * 1000)
)
})
},
'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