import { Add, CancelRounded, EditCalendar, ExpandCircleDown, FilterAlt, PriorityHigh, Style, Unarchive, } 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, GetArchivedChores, 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 [archivedChores, setArchivedChores] = useState(null) 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) => { var newChores = chores.map(chore => { if (chore.id === updatedChore.id) { return updatedChore } return chore }) var newFilteredChores = filteredChores.map(chore => { if (chore.id === updatedChore.id) { return updatedChore } return chore }) if (event === 'archive') { newChores = newChores.filter(chore => chore.id !== updatedChore.id) newFilteredChores = newFilteredChores.filter( chore => chore.id !== updatedChore.id, ) if (archivedChores !== null) { setArchivedChores([...archivedChores, updatedChore]) } } if (event === 'unarchive') { newChores.push(updatedChore) newFilteredChores.push(updatedChore) setArchivedChores( archivedChores.filter(chore => chore.id !== updatedChore.id), ) } setChores(newChores) setFilteredChores(newFilteredChores) setChoreSections(sectionSorter('due_date', newChores)) switch (event) { case 'completed': setSnackBarMessage('Completed') break case 'skipped': setSnackBarMessage('Skipped') break case 'rescheduled': setSnackBarMessage('Rescheduled') break case 'unarchive': setSnackBarMessage('Unarchive') break case 'archive': setSnackBarMessage('Archived') 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) setChoreSections(sectionSorter('due_date', newChores)) } 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 /> {Object.keys(FILTERS).map((filter, index) => ( { const filterFunction = FILTERS[filter] const filteredChores = filterFunction.length === 2 ? filterFunction(chores, userProfile.id) : filterFunction(chores) setFilteredChores(filteredChores) setSelectedFilter(filter) handleFilterMenuClose() }} > {filter} {FILTERS[filter].length === 2 ? FILTERS[filter](chores, userProfile.id).length : FILTERS[filter](chores).length} ))} {selectedFilter.startsWith('Label: ') || (selectedFilter.startsWith('Priority: ') && ( { setFilteredChores(chores) setSelectedFilter('All') }} > Cancel All Filters ))} {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 => ( ))} ) })} )} {archivedChores === null && ( )} {archivedChores !== null && ( <> {archivedChores?.length} } > Archived {archivedChores?.map(chore => ( ))} )} { 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