- Support nest sub tasks
- Support filters in ChoreGrouper - completion window only available if due date selected - Add SortAndGrouping Component : support Filter by Assignee - update Notification Switch to align left of the text - Support caching filters
This commit is contained in:
commit
467fc935f0
9 changed files with 679 additions and 224 deletions
|
@ -3,7 +3,11 @@ import { TASK_COLOR } from './Colors.jsx'
|
||||||
|
|
||||||
const priorityOrder = [1, 2, 3, 4, 0]
|
const priorityOrder = [1, 2, 3, 4, 0]
|
||||||
|
|
||||||
export const ChoresGrouper = (groupBy, chores) => {
|
export const ChoresGrouper = (groupBy, chores, filter) => {
|
||||||
|
if (filter) {
|
||||||
|
chores = chores.filter(chore => filter(chore))
|
||||||
|
}
|
||||||
|
|
||||||
// sort by priority then due date:
|
// sort by priority then due date:
|
||||||
chores.sort(ChoreSorter)
|
chores.sort(ChoreSorter)
|
||||||
var groups = []
|
var groups = []
|
||||||
|
@ -172,3 +176,12 @@ export const notInCompletionWindow = chore => {
|
||||||
moment().add(chore.completionWindow, 'hours') < moment(chore.nextDueDate)
|
moment().add(chore.completionWindow, 'hours') < moment(chore.nextDueDate)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export const ChoreFilters = userProfile => ({
|
||||||
|
anyone: () => true,
|
||||||
|
assigned_to_me: chore => {
|
||||||
|
return chore.assignedTo && chore.assignedTo === userProfile.id
|
||||||
|
},
|
||||||
|
assigned_to_others: chore => {
|
||||||
|
return chore.assignedTo && chore.assignedTo !== userProfile.id
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
@ -208,7 +208,9 @@ const ChoreEdit = () => {
|
||||||
notificationMetadata: notificationMetadata,
|
notificationMetadata: notificationMetadata,
|
||||||
thingTrigger: thingTrigger,
|
thingTrigger: thingTrigger,
|
||||||
points: points < 0 ? null : points,
|
points: points < 0 ? null : points,
|
||||||
completionWindow: completionWindow < 0 ? null : completionWindow,
|
completionWindow:
|
||||||
|
// if completionWindow is -1 then set it to null or dueDate is null
|
||||||
|
completionWindow < 0 || dueDate === null ? null : completionWindow,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
}
|
}
|
||||||
let SaveFunction = CreateChore
|
let SaveFunction = CreateChore
|
||||||
|
@ -596,7 +598,8 @@ const ChoreEdit = () => {
|
||||||
<FormHelperText>{errors.dueDate}</FormHelperText>
|
<FormHelperText>{errors.dueDate}</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
{dueDate && (
|
||||||
|
<>
|
||||||
<FormControl orientation='horizontal'>
|
<FormControl orientation='horizontal'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={completionWindow != -1}
|
checked={completionWindow != -1}
|
||||||
|
@ -653,6 +656,8 @@ const ChoreEdit = () => {
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{!['once', 'no_repeat'].includes(frequencyType) && (
|
{!['once', 'no_repeat'].includes(frequencyType) && (
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
|
@ -975,7 +980,12 @@ const ChoreEdit = () => {
|
||||||
p: 1,
|
p: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SubTasks editMode={true} tasks={subTasks} setTasks={setSubTasks} />
|
<SubTasks
|
||||||
|
editMode={true}
|
||||||
|
tasks={subTasks}
|
||||||
|
setTasks={setSubTasks}
|
||||||
|
choreId={choreId}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
<FormControl sx={{ mt: 1 }}>
|
<FormControl sx={{ mt: 1 }}>
|
||||||
|
|
|
@ -44,7 +44,7 @@ import { useLabels } from '../Labels/LabelQueries'
|
||||||
import ChoreCard from './ChoreCard'
|
import ChoreCard from './ChoreCard'
|
||||||
import IconButtonWithMenu from './IconButtonWithMenu'
|
import IconButtonWithMenu from './IconButtonWithMenu'
|
||||||
|
|
||||||
import { ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
|
||||||
import TaskInput from '../components/AddTaskModal'
|
import TaskInput from '../components/AddTaskModal'
|
||||||
import {
|
import {
|
||||||
canScheduleNotification,
|
canScheduleNotification,
|
||||||
|
@ -52,6 +52,7 @@ import {
|
||||||
} from './LocalNotificationScheduler'
|
} from './LocalNotificationScheduler'
|
||||||
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
||||||
import Sidepanel from './Sidepanel'
|
import Sidepanel from './Sidepanel'
|
||||||
|
import SortAndGrouping from './SortAndGrouping'
|
||||||
|
|
||||||
const MyChores = () => {
|
const MyChores = () => {
|
||||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||||
|
@ -60,7 +61,7 @@ const MyChores = () => {
|
||||||
const [chores, setChores] = useState([])
|
const [chores, setChores] = useState([])
|
||||||
const [archivedChores, setArchivedChores] = useState(null)
|
const [archivedChores, setArchivedChores] = useState(null)
|
||||||
const [filteredChores, setFilteredChores] = useState([])
|
const [filteredChores, setFilteredChores] = useState([])
|
||||||
const [selectedFilter, setSelectedFilter] = useState('All')
|
const [searchFilter, setSearchFilter] = useState('All')
|
||||||
const [choreSections, setChoreSections] = useState([])
|
const [choreSections, setChoreSections] = useState([])
|
||||||
const [activeTextField, setActiveTextField] = useState('task')
|
const [activeTextField, setActiveTextField] = useState('task')
|
||||||
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
const [taskInputFocus, setTaskInputFocus] = useState(0)
|
||||||
|
@ -72,6 +73,9 @@ const MyChores = () => {
|
||||||
const [openChoreSections, setOpenChoreSections] = useState(
|
const [openChoreSections, setOpenChoreSections] = useState(
|
||||||
JSON.parse(localStorage.getItem('openChoreSections')) || {},
|
JSON.parse(localStorage.getItem('openChoreSections')) || {},
|
||||||
)
|
)
|
||||||
|
const [selectedChoreFilter, setSelectedChoreFilter] = useState(
|
||||||
|
JSON.parse(localStorage.getItem('selectedChoreFilter')) || 'anyone',
|
||||||
|
)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [performers, setPerformers] = useState([])
|
const [performers, setPerformers] = useState([])
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
@ -172,6 +176,10 @@ const MyChores = () => {
|
||||||
setOpenChoreSections(value)
|
setOpenChoreSections(value)
|
||||||
localStorage.setItem('openChoreSections', JSON.stringify(value))
|
localStorage.setItem('openChoreSections', JSON.stringify(value))
|
||||||
}
|
}
|
||||||
|
const setSelectedChoreFilterWithCache = value => {
|
||||||
|
setSelectedChoreFilter(value)
|
||||||
|
localStorage.setItem('selectedChoreFilter', JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
const updateChores = newChore => {
|
const updateChores = newChore => {
|
||||||
const newChores = chores
|
const newChores = chores
|
||||||
|
@ -179,7 +187,7 @@ const MyChores = () => {
|
||||||
setChores(newChores)
|
setChores(newChores)
|
||||||
setFilteredChores(newChores)
|
setFilteredChores(newChores)
|
||||||
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
setChoreSections(ChoresGrouper(selectedChoreSection, newChores))
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}
|
}
|
||||||
const handleMenuOutsideClick = event => {
|
const handleMenuOutsideClick = event => {
|
||||||
if (
|
if (
|
||||||
|
@ -208,14 +216,14 @@ const MyChores = () => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
setFilteredChores(labelFiltered)
|
setFilteredChores(labelFiltered)
|
||||||
setSelectedFilter('Label: ' + label.name)
|
setSearchFilter('Label: ' + label.name)
|
||||||
} else if (chipClicked.priority) {
|
} else if (chipClicked.priority) {
|
||||||
const priority = chipClicked.priority
|
const priority = chipClicked.priority
|
||||||
const priorityFiltered = chores.filter(
|
const priorityFiltered = chores.filter(
|
||||||
chore => chore.priority === priority,
|
chore => chore.priority === priority,
|
||||||
)
|
)
|
||||||
setFilteredChores(priorityFiltered)
|
setFilteredChores(priorityFiltered)
|
||||||
setSelectedFilter('Priority: ' + priority)
|
setSearchFilter('Priority: ' + priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,8 +313,8 @@ const MyChores = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSearchChange = e => {
|
const handleSearchChange = e => {
|
||||||
if (selectedFilter !== 'All') {
|
if (searchFilter !== 'All') {
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}
|
}
|
||||||
const search = e.target.value
|
const search = e.target.value
|
||||||
if (search === '') {
|
if (search === '') {
|
||||||
|
@ -418,17 +426,28 @@ const MyChores = () => {
|
||||||
<Search />
|
<Search />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
<SortAndGrouping
|
||||||
<IconButtonWithMenu
|
|
||||||
title='Group by'
|
title='Group by'
|
||||||
k={'icon-menu-group-by'}
|
k={'icon-menu-group-by'}
|
||||||
icon={<Sort />}
|
icon={<Sort />}
|
||||||
options={[
|
|
||||||
{ name: 'Due Date', value: 'due_date' },
|
|
||||||
{ name: 'Priority', value: 'priority' },
|
|
||||||
{ name: 'Labels', value: 'labels' },
|
|
||||||
]}
|
|
||||||
selectedItem={selectedChoreSection}
|
selectedItem={selectedChoreSection}
|
||||||
|
selectedFilter={selectedChoreFilter}
|
||||||
|
setFilter={filter => {
|
||||||
|
setSelectedChoreFilterWithCache(filter)
|
||||||
|
const section = ChoresGrouper(
|
||||||
|
selectedChoreSection,
|
||||||
|
chores,
|
||||||
|
ChoreFilters(userProfile)[filter],
|
||||||
|
)
|
||||||
|
setChoreSections(section)
|
||||||
|
setOpenChoreSectionsWithCache(
|
||||||
|
// open all sections by default
|
||||||
|
Object.keys(section).reduce((acc, key) => {
|
||||||
|
acc[key] = true
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
onItemSelect={selected => {
|
onItemSelect={selected => {
|
||||||
const section = ChoresGrouper(selected.value, chores)
|
const section = ChoresGrouper(selected.value, chores)
|
||||||
setChoreSections(section)
|
setChoreSections(section)
|
||||||
|
@ -441,7 +460,7 @@ const MyChores = () => {
|
||||||
}, {}),
|
}, {}),
|
||||||
)
|
)
|
||||||
setFilteredChores(chores)
|
setFilteredChores(chores)
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}}
|
}}
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
/>
|
/>
|
||||||
|
@ -454,12 +473,12 @@ const MyChores = () => {
|
||||||
k={'icon-menu-priority-filter'}
|
k={'icon-menu-priority-filter'}
|
||||||
icon={<PriorityHigh />}
|
icon={<PriorityHigh />}
|
||||||
options={Priorities}
|
options={Priorities}
|
||||||
selectedItem={selectedFilter}
|
selectedItem={searchFilter}
|
||||||
onItemSelect={selected => {
|
onItemSelect={selected => {
|
||||||
handleLabelFiltering({ priority: selected.value })
|
handleLabelFiltering({ priority: selected.value })
|
||||||
}}
|
}}
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
isActive={selectedFilter.startsWith('Priority: ')}
|
isActive={searchFilter.startsWith('Priority: ')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButtonWithMenu
|
<IconButtonWithMenu
|
||||||
|
@ -467,11 +486,11 @@ const MyChores = () => {
|
||||||
label={' Labels'}
|
label={' Labels'}
|
||||||
icon={<Style />}
|
icon={<Style />}
|
||||||
options={userLabels}
|
options={userLabels}
|
||||||
selectedItem={selectedFilter}
|
selectedItem={searchFilter}
|
||||||
onItemSelect={selected => {
|
onItemSelect={selected => {
|
||||||
handleLabelFiltering({ label: selected })
|
handleLabelFiltering({ label: selected })
|
||||||
}}
|
}}
|
||||||
isActive={selectedFilter.startsWith('Label: ')}
|
isActive={searchFilter.startsWith('Label: ')}
|
||||||
mouseClickHandler={handleMenuOutsideClick}
|
mouseClickHandler={handleMenuOutsideClick}
|
||||||
useChips
|
useChips
|
||||||
/>
|
/>
|
||||||
|
@ -481,9 +500,7 @@ const MyChores = () => {
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
startDecorator={<Grain />}
|
startDecorator={<Grain />}
|
||||||
color={
|
color={
|
||||||
selectedFilter &&
|
searchFilter && FILTERS[searchFilter] && searchFilter != 'All'
|
||||||
FILTERS[selectedFilter] &&
|
|
||||||
selectedFilter != 'All'
|
|
||||||
? 'primary'
|
? 'primary'
|
||||||
: 'neutral'
|
: 'neutral'
|
||||||
}
|
}
|
||||||
|
@ -519,15 +536,13 @@ const MyChores = () => {
|
||||||
? filterFunction(chores, userProfile.id)
|
? filterFunction(chores, userProfile.id)
|
||||||
: filterFunction(chores)
|
: filterFunction(chores)
|
||||||
setFilteredChores(filteredChores)
|
setFilteredChores(filteredChores)
|
||||||
setSelectedFilter(filter)
|
setSearchFilter(filter)
|
||||||
handleFilterMenuClose()
|
handleFilterMenuClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filter}
|
{filter}
|
||||||
<Chip
|
<Chip
|
||||||
color={
|
color={searchFilter === filter ? 'primary' : 'neutral'}
|
||||||
selectedFilter === filter ? 'primary' : 'neutral'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{FILTERS[filter].length === 2
|
{FILTERS[filter].length === 2
|
||||||
? FILTERS[filter](chores, userProfile.id).length
|
? FILTERS[filter](chores, userProfile.id).length
|
||||||
|
@ -536,13 +551,13 @@ const MyChores = () => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{selectedFilter.startsWith('Label: ') ||
|
{searchFilter.startsWith('Label: ') ||
|
||||||
(selectedFilter.startsWith('Priority: ') && (
|
(searchFilter.startsWith('Priority: ') && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={`filter-list-cancel-all-filters`}
|
key={`filter-list-cancel-all-filters`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilteredChores(chores)
|
setFilteredChores(chores)
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel All Filters
|
Cancel All Filters
|
||||||
|
@ -553,23 +568,23 @@ const MyChores = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedFilter !== 'All' && (
|
{searchFilter !== 'All' && (
|
||||||
<Chip
|
<Chip
|
||||||
level='title-md'
|
level='title-md'
|
||||||
gutterBottom
|
gutterBottom
|
||||||
color='warning'
|
color='warning'
|
||||||
label={selectedFilter}
|
label={searchFilter}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setFilteredChores(chores)
|
setFilteredChores(chores)
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}}
|
}}
|
||||||
endDecorator={<CancelRounded />}
|
endDecorator={<CancelRounded />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilteredChores(chores)
|
setFilteredChores(chores)
|
||||||
setSelectedFilter('All')
|
setSearchFilter('All')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Current Filter: {selectedFilter}
|
Current Filter: {searchFilter}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{filteredChores.length === 0 && (
|
{filteredChores.length === 0 && (
|
||||||
|
@ -608,7 +623,7 @@ const MyChores = () => {
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{(searchTerm?.length > 0 || selectedFilter !== 'All') &&
|
{(searchTerm?.length > 0 || searchFilter !== 'All') &&
|
||||||
filteredChores.map(chore => (
|
filteredChores.map(chore => (
|
||||||
<ChoreCard
|
<ChoreCard
|
||||||
key={`filtered-${chore.id} `}
|
key={`filtered-${chore.id} `}
|
||||||
|
@ -620,7 +635,7 @@ const MyChores = () => {
|
||||||
onChipClick={handleLabelFiltering}
|
onChipClick={handleLabelFiltering}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{searchTerm.length === 0 && selectedFilter === 'All' && (
|
{searchTerm.length === 0 && searchFilter === 'All' && (
|
||||||
<AccordionGroup transition='0.2s ease' disableDivider>
|
<AccordionGroup transition='0.2s ease' disableDivider>
|
||||||
{choreSections.map((section, index) => {
|
{choreSections.map((section, index) => {
|
||||||
if (section.content.length === 0) return null
|
if (section.content.length === 0) return null
|
||||||
|
|
|
@ -13,7 +13,7 @@ const Sidepanel = ({ chores }) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const generateChoreDuePieChartData = chores => {
|
const generateChoreDuePieChartData = chores => {
|
||||||
const groups = ChoresGrouper('due_date', chores)
|
const groups = ChoresGrouper('due_date', chores, null)
|
||||||
return groups
|
return groups
|
||||||
.map(group => {
|
.map(group => {
|
||||||
return {
|
return {
|
||||||
|
|
194
src/views/Chores/SortAndGrouping.jsx
Normal file
194
src/views/Chores/SortAndGrouping.jsx
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Radio,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/joy'
|
||||||
|
import IconButton from '@mui/joy/IconButton'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||||
|
|
||||||
|
const SortAndGrouping = ({
|
||||||
|
label,
|
||||||
|
k,
|
||||||
|
icon,
|
||||||
|
onItemSelect,
|
||||||
|
selectedItem,
|
||||||
|
setSelectedItem,
|
||||||
|
selectedFilter,
|
||||||
|
setFilter,
|
||||||
|
isActive,
|
||||||
|
useChips,
|
||||||
|
title,
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const menuRef = useRef(null)
|
||||||
|
|
||||||
|
const handleMenuOpen = event => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMenuOutsideClick = event => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||||
|
handleMenuClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleMenuOutsideClick)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleMenuOutsideClick)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!label && (
|
||||||
|
<IconButton
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
variant='outlined'
|
||||||
|
color={isActive ? 'primary' : 'neutral'}
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label ? label : null}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{label && (
|
||||||
|
<Button
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
variant='outlined'
|
||||||
|
color={isActive ? 'primary' : 'neutral'}
|
||||||
|
size='sm'
|
||||||
|
startDecorator={icon}
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
key={k}
|
||||||
|
ref={menuRef}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
sx={{
|
||||||
|
'& .MuiMenuItem-root': {
|
||||||
|
padding: '8px 16px', // Consistent padding for menu items
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem key={`${k}-title`} disabled>
|
||||||
|
<Typography level='body-sm' fontWeight='lg'>
|
||||||
|
Group By
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{ name: 'Due Date', value: 'due_date' },
|
||||||
|
{ name: 'Priority', value: 'priority' },
|
||||||
|
{ name: 'Labels', value: 'labels' },
|
||||||
|
].map(item => (
|
||||||
|
<MenuItem
|
||||||
|
key={`${k}-${item?.value}`}
|
||||||
|
onClick={() => {
|
||||||
|
onItemSelect(item)
|
||||||
|
setSelectedItem?.(item.name)
|
||||||
|
handleMenuClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{useChips ? (
|
||||||
|
<Chip
|
||||||
|
size='sm'
|
||||||
|
sx={{
|
||||||
|
backgroundColor: item.color ? item.color : null,
|
||||||
|
color: getTextColorFromBackgroundColor(item.color),
|
||||||
|
fontWeight: 'md',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{item?.icon}
|
||||||
|
<Typography level='body-sm' sx={{ ml: 1 }}>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<MenuItem key={`${k}-quick-filter`} disabled>
|
||||||
|
<Typography level='body-sm' fontWeight='lg'>
|
||||||
|
Quick Filters
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem key={`${k}-assignee-title`} disabled>
|
||||||
|
<Typography level='body-xs' fontWeight='md'>
|
||||||
|
Assigned to:
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
key={`${k}-assignee-anyone`}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter('anyone')
|
||||||
|
handleMenuClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio checked={selectedFilter === 'anyone'} variant='outlined' />
|
||||||
|
<Typography level='body-sm'>Anyone</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
key={`${k}-assignee-assigned-to-me`}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter('assigned_to_me')
|
||||||
|
handleMenuClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
checked={selectedFilter === 'assigned_to_me'}
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
|
<Typography level='body-sm'>Assigned to me</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
key={`${k}-assignee-assigned-to-others`}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter('assigned_to_others')
|
||||||
|
handleMenuClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
checked={selectedFilter === 'assigned_to_others'}
|
||||||
|
variant='outlined'
|
||||||
|
/>
|
||||||
|
<Typography level='body-sm'>Assigned to others</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortAndGrouping
|
|
@ -173,7 +173,14 @@ const NotificationSetting = () => {
|
||||||
}}
|
}}
|
||||||
color={deviceNotification ? 'success' : 'neutral'}
|
color={deviceNotification ? 'success' : 'neutral'}
|
||||||
variant={deviceNotification ? 'solid' : 'outlined'}
|
variant={deviceNotification ? 'solid' : 'outlined'}
|
||||||
sx={{ mr: 1 }}
|
slotProps={{
|
||||||
|
endDecorator: {
|
||||||
|
sx: {
|
||||||
|
minWidth: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>Device Notification</FormLabel>
|
<FormLabel>Device Notification</FormLabel>
|
||||||
|
@ -321,7 +328,6 @@ const NotificationSetting = () => {
|
||||||
|
|
||||||
<FormControl orientation='horizontal'>
|
<FormControl orientation='horizontal'>
|
||||||
<Switch
|
<Switch
|
||||||
sx={{ mr: 1 }}
|
|
||||||
checked={chatID !== 0}
|
checked={chatID !== 0}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -342,12 +348,14 @@ const NotificationSetting = () => {
|
||||||
}}
|
}}
|
||||||
color={chatID !== 0 ? 'success' : 'neutral'}
|
color={chatID !== 0 ? 'success' : 'neutral'}
|
||||||
variant={chatID !== 0 ? 'solid' : 'outlined'}
|
variant={chatID !== 0 ? 'solid' : 'outlined'}
|
||||||
// endDecorator={chatID !== 0 ? 'On' : 'Off'}
|
|
||||||
slotProps={{
|
slotProps={{
|
||||||
endDecorator: {
|
endDecorator: {
|
||||||
sx: {},
|
sx: {
|
||||||
|
minWidth: 24,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>Custom Notification</FormLabel>
|
<FormLabel>Custom Notification</FormLabel>
|
||||||
|
@ -372,7 +380,7 @@ const NotificationSetting = () => {
|
||||||
<Option value='0'>None</Option>
|
<Option value='0'>None</Option>
|
||||||
<Option value='1'>Telegram</Option>
|
<Option value='1'>Telegram</Option>
|
||||||
<Option value='2'>Pushover</Option>
|
<Option value='2'>Pushover</Option>
|
||||||
<Option value='3'>Webhook</Option>
|
<Option value='3'>Webhooks</Option>
|
||||||
</Select>
|
</Select>
|
||||||
{notificationTarget === '1' && (
|
{notificationTarget === '1' && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -232,7 +232,7 @@ const UserActivites = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateChoreDuePieChartData = chores => {
|
const generateChoreDuePieChartData = chores => {
|
||||||
const groups = ChoresGrouper('due_date', chores)
|
const groups = ChoresGrouper('due_date', chores, null)
|
||||||
return groups
|
return groups
|
||||||
.map(group => {
|
.map(group => {
|
||||||
return {
|
return {
|
||||||
|
@ -245,7 +245,7 @@ const UserActivites = () => {
|
||||||
.filter(item => item.value > 0)
|
.filter(item => item.value > 0)
|
||||||
}
|
}
|
||||||
const generateChorePriorityPieChartData = chores => {
|
const generateChorePriorityPieChartData = chores => {
|
||||||
const groups = ChoresGrouper('priority', chores)
|
const groups = ChoresGrouper('priority', chores, null)
|
||||||
return groups
|
return groups
|
||||||
.map(group => {
|
.map(group => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -76,12 +76,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||||
const { userProfile } = useContext(UserContext)
|
const { userProfile } = useContext(UserContext)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [taskText, setTaskText] = useState('')
|
const [taskText, setTaskText] = useState('')
|
||||||
const debounceParsing = useDebounce(taskText, 300)
|
const debounceParsing = useDebounce(taskText, 30)
|
||||||
const [taskTitle, setTaskTitle] = useState('')
|
const [taskTitle, setTaskTitle] = useState('')
|
||||||
const [openModal, setOpenModal] = useState(false)
|
const [openModal, setOpenModal] = useState(false)
|
||||||
const textareaRef = useRef(null)
|
const textareaRef = useRef(null)
|
||||||
const mainInputRef = useRef(null)
|
const mainInputRef = useRef(null)
|
||||||
const [priority, setPriority] = useState('0')
|
const [priority, setPriority] = useState(0)
|
||||||
const [dueDate, setDueDate] = useState(null)
|
const [dueDate, setDueDate] = useState(null)
|
||||||
const [description, setDescription] = useState(null)
|
const [description, setDescription] = useState(null)
|
||||||
const [frequency, setFrequency] = useState(null)
|
const [frequency, setFrequency] = useState(null)
|
||||||
|
@ -436,7 +436,6 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||||
priority: priority || 0,
|
priority: priority || 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
frequencyType: 'once',
|
frequencyType: 'once',
|
||||||
notificationMetadata: {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frequency) {
|
if (frequency) {
|
||||||
|
@ -451,7 +450,12 @@ const TaskInput = ({ autoFocus, onChoreUpdate }) => {
|
||||||
|
|
||||||
CreateChore(chore).then(resp => {
|
CreateChore(chore).then(resp => {
|
||||||
resp.json().then(data => {
|
resp.json().then(data => {
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
console.error('Error creating chore:', data)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
|
onChoreUpdate({ ...chore, id: data.res, nextDueDate: chore.dueDate })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,15 @@ import {
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { Add, Delete, DragIndicator, Edit } from '@mui/icons-material'
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Delete,
|
||||||
|
DragIndicator,
|
||||||
|
Edit,
|
||||||
|
ExpandMore,
|
||||||
|
KeyboardReturn,
|
||||||
|
PlaylistAdd,
|
||||||
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
@ -19,23 +27,41 @@ import {
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { CompleteSubTask } from '../../utils/Fetcher'
|
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||||
|
|
||||||
function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
function SortableItem({
|
||||||
|
task,
|
||||||
|
index,
|
||||||
|
handleToggle,
|
||||||
|
handleDelete,
|
||||||
|
handleAddSubtask,
|
||||||
|
allTasks,
|
||||||
|
setTasks,
|
||||||
|
level = 0,
|
||||||
|
editMode,
|
||||||
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
useSortable({ id: task.id })
|
useSortable({ id: task.id })
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [editedText, setEditedText] = useState(task.name)
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [showAddSubtask, setShowAddSubtask] = useState(false)
|
||||||
|
const [newSubtask, setNewSubtask] = useState('')
|
||||||
|
|
||||||
|
// Find child tasks
|
||||||
|
const childTasks = allTasks.filter(t => t.parentId === task.id)
|
||||||
|
const hasChildren = childTasks.length > 0
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
flexDirection: { xs: 'column', sm: 'row' }, // Responsive style
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
|
paddingLeft: `${level * 24}px`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [editedText, setEditedText] = useState(task.name)
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
}
|
}
|
||||||
|
@ -43,15 +69,59 @@ function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
task.name = editedText
|
task.name = editedText
|
||||||
|
// Update the task in the parent component
|
||||||
|
setTasks(prevTasks =>
|
||||||
|
prevTasks.map(t => (t.id === task.id ? { ...t, name: editedText } : t)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExpandClick = () => {
|
||||||
|
setExpanded(!expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSubtaskClick = () => {
|
||||||
|
setShowAddSubtask(!showAddSubtask)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitNewSubtask = () => {
|
||||||
|
if (!newSubtask.trim()) return
|
||||||
|
|
||||||
|
handleAddSubtask(task.id, newSubtask)
|
||||||
|
setNewSubtask('')
|
||||||
|
setShowAddSubtask(false)
|
||||||
|
setExpanded(true) // Auto-expand to show the new subtask
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
submitNewSubtask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ListItem ref={setNodeRef} style={style} {...attributes}>
|
<ListItem ref={setNodeRef} style={style} {...attributes}>
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<IconButton {...listeners} {...attributes}>
|
<IconButton {...listeners} {...attributes} size='sm'>
|
||||||
<DragIndicator />
|
<DragIndicator />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasChildren && (
|
||||||
|
<IconButton
|
||||||
|
size='sm'
|
||||||
|
variant='plain'
|
||||||
|
color='neutral'
|
||||||
|
onClick={handleExpandClick}
|
||||||
|
>
|
||||||
|
{expanded ? <ExpandMore /> : <ChevronRight />}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasChildren && level > 0 && (
|
||||||
|
<Box sx={{ width: 28 }} /> // Spacer for alignment not sure of better way for now it's good
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -62,9 +132,8 @@ function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
>
|
>
|
||||||
{!editMode && (
|
{!editMode && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={task.completedAt}
|
checked={!!task.completedAt}
|
||||||
onChange={() => handleToggle(task.id)}
|
onChange={() => handleToggle(task.id)}
|
||||||
overlay={!editMode}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box
|
||||||
|
@ -75,6 +144,11 @@ function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!editMode) {
|
||||||
|
handleToggle(task.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Input
|
<Input
|
||||||
|
@ -101,8 +175,9 @@ function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
{task.completedAt && (
|
{task.completedAt && (
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'block', sm: 'inline' }, // Responsive style
|
display: { xs: 'block', sm: 'inline' },
|
||||||
color: 'text.secondary',
|
color: 'text.secondary',
|
||||||
|
fontSize: 'sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{new Date(task.completedAt).toLocaleString()}
|
{new Date(task.completedAt).toLocaleString()}
|
||||||
|
@ -110,79 +185,216 @@ function SortableItem({ task, index, handleToggle, handleDelete, editMode }) {
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{editMode && (
|
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<IconButton variant='soft' onClick={handleEdit}>
|
{editMode && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
variant='soft'
|
||||||
|
color='primary'
|
||||||
|
size='sm'
|
||||||
|
onClick={handleAddSubtaskClick}
|
||||||
|
title='Add subtask'
|
||||||
|
>
|
||||||
|
<PlaylistAdd />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton variant='soft' size='sm' onClick={handleEdit}>
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color='danger'
|
color='danger'
|
||||||
|
size='sm'
|
||||||
onClick={() => handleDelete(task.id)}
|
onClick={() => handleDelete(task.id)}
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{/* Add subtask input field */}
|
||||||
|
{showAddSubtask && (
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
paddingLeft: `${(level + 1) * 24}px`,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', width: '100%', gap: 1 }}>
|
||||||
|
<Input
|
||||||
|
placeholder='Add new subtask...'
|
||||||
|
value={newSubtask}
|
||||||
|
onChange={e => setNewSubtask(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<IconButton onClick={submitNewSubtask} size='sm'>
|
||||||
|
<KeyboardReturn />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Child tasks */}
|
||||||
|
{hasChildren && expanded && (
|
||||||
|
<Box sx={{ paddingLeft: `${level * 24}px` }}>
|
||||||
|
{childTasks
|
||||||
|
.sort((a, b) => a.orderId - b.orderId)
|
||||||
|
.map((childTask, childIndex) => (
|
||||||
|
<SortableItem
|
||||||
|
key={childTask.id}
|
||||||
|
task={childTask}
|
||||||
|
index={childIndex}
|
||||||
|
handleToggle={handleToggle}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
handleAddSubtask={handleAddSubtask}
|
||||||
|
allTasks={allTasks}
|
||||||
|
setTasks={setTasks}
|
||||||
|
level={level + 1}
|
||||||
|
editMode={editMode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</ListItem>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubTasks = ({ editMode = true, choreId = 0, tasks, setTasks }) => {
|
const SubTasks = ({ editMode = true, choreId = 0, tasks = [], setTasks }) => {
|
||||||
const [newTask, setNewTask] = useState('')
|
const [newTask, setNewTask] = useState('')
|
||||||
|
|
||||||
|
const topLevelTasks = tasks.filter(task => task.parentId === null)
|
||||||
|
|
||||||
const handleToggle = taskId => {
|
const handleToggle = taskId => {
|
||||||
const updatedTask = tasks.find(task => task.id === taskId)
|
const updatedTask = tasks.find(task => task.id === taskId)
|
||||||
updatedTask.completedAt = updatedTask.completedAt
|
const newCompletedAt = updatedTask.completedAt
|
||||||
? null
|
? null
|
||||||
: new Date().toISOString()
|
: new Date().toISOString()
|
||||||
|
|
||||||
|
// Update the task
|
||||||
const updatedTasks = tasks.map(task =>
|
const updatedTasks = tasks.map(task =>
|
||||||
task.id === taskId ? updatedTask : task,
|
task.id === taskId ? { ...task, completedAt: newCompletedAt } : task,
|
||||||
)
|
)
|
||||||
CompleteSubTask(
|
|
||||||
taskId,
|
// If completing a task, also complete all child tasks
|
||||||
Number(choreId),
|
if (newCompletedAt) {
|
||||||
updatedTask.completedAt ? new Date().toISOString() : null,
|
const completeChildren = parentId => {
|
||||||
).then(res => {
|
const children = updatedTasks.filter(t => t.parentId === parentId)
|
||||||
|
children.forEach(child => {
|
||||||
|
const index = updatedTasks.findIndex(t => t.id === child.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
updatedTasks[index] = {
|
||||||
|
...updatedTasks[index],
|
||||||
|
completedAt: newCompletedAt,
|
||||||
|
}
|
||||||
|
completeChildren(child.id) // Recursively complete grandchildren
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
completeChildren(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompleteSubTask(taskId, Number(choreId), newCompletedAt).then(res => {
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
console.log('Error updating task')
|
console.log('Error updating task')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setTasks(updatedTasks)
|
setTasks(updatedTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = taskId => {
|
const handleDelete = taskId => {
|
||||||
setTasks(tasks.filter(task => task.id !== taskId))
|
// Find all descendant tasks to delete
|
||||||
|
const findDescendants = id => {
|
||||||
|
const descendants = []
|
||||||
|
const children = tasks.filter(t => t.parentId === id)
|
||||||
|
|
||||||
|
children.forEach(child => {
|
||||||
|
descendants.push(child.id)
|
||||||
|
descendants.push(...findDescendants(child.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
return descendants
|
||||||
|
}
|
||||||
|
|
||||||
|
const descendantIds = findDescendants(taskId)
|
||||||
|
const idsToDelete = [taskId, ...descendantIds]
|
||||||
|
|
||||||
|
// Filter out the task and all its descendants
|
||||||
|
const updatedTasks = tasks
|
||||||
|
.filter(task => !idsToDelete.includes(task.id))
|
||||||
|
.map((task, index) => ({
|
||||||
|
...task,
|
||||||
|
orderId: task.parentId === null ? index : task.orderId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setTasks(updatedTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (!newTask.trim()) return
|
if (!newTask.trim()) return
|
||||||
setTasks([
|
|
||||||
...tasks,
|
const newTaskObj = {
|
||||||
{
|
|
||||||
name: newTask,
|
name: newTask,
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
orderId: tasks.length,
|
orderId: topLevelTasks.length,
|
||||||
},
|
parentId: null,
|
||||||
])
|
id: (tasks.length + 1) * -1, // Temporary negative ID
|
||||||
|
}
|
||||||
|
|
||||||
|
setTasks([...tasks, newTaskObj])
|
||||||
setNewTask('')
|
setNewTask('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddSubtask = (parentId, name) => {
|
||||||
|
if (!name.trim()) return
|
||||||
|
|
||||||
|
// Find siblings to determine orderId
|
||||||
|
const siblings = tasks.filter(t => t.parentId === parentId)
|
||||||
|
|
||||||
|
const newSubtask = {
|
||||||
|
name,
|
||||||
|
completedAt: null,
|
||||||
|
orderId: siblings.length,
|
||||||
|
parentId,
|
||||||
|
id: (tasks.length + 1) * -1, // Temporary negative ID
|
||||||
|
}
|
||||||
|
|
||||||
|
setTasks([...tasks, newSubtask])
|
||||||
|
}
|
||||||
|
|
||||||
const onDragEnd = event => {
|
const onDragEnd = event => {
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
if (active.id !== over.id) {
|
if (!over || active.id === over.id) return
|
||||||
|
|
||||||
setTasks(items => {
|
setTasks(items => {
|
||||||
const oldIndex = items.findIndex(item => item.id === active.id)
|
const oldIndex = items.findIndex(item => item.id === active.id)
|
||||||
const newIndex = items.findIndex(item => item.id === over.id)
|
const newIndex = items.findIndex(item => item.id === over.id)
|
||||||
|
|
||||||
|
if (oldIndex === -1 || newIndex === -1) return items
|
||||||
|
|
||||||
|
const activeItem = items[oldIndex]
|
||||||
|
const overItem = items[newIndex]
|
||||||
|
|
||||||
const reorderedItems = arrayMove(items, oldIndex, newIndex)
|
const reorderedItems = arrayMove(items, oldIndex, newIndex)
|
||||||
return reorderedItems.map((item, index) => ({
|
|
||||||
...item,
|
const parentId = overItem.parentId
|
||||||
orderId: index,
|
const siblings = reorderedItems.filter(item => item.parentId === parentId)
|
||||||
}))
|
|
||||||
})
|
return reorderedItems.map(item => {
|
||||||
|
if (item.id === activeItem.id) {
|
||||||
|
return { ...item, parentId, orderId: siblings.indexOf(item) }
|
||||||
}
|
}
|
||||||
|
return item.parentId === parentId
|
||||||
|
? { ...item, orderId: siblings.indexOf(item) }
|
||||||
|
: item
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyPress = event => {
|
const handleKeyPress = event => {
|
||||||
|
@ -191,38 +403,37 @@ const SubTasks = ({ editMode = true, choreId = 0, tasks, setTasks }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort tasks by orderId before rendering
|
|
||||||
const sortedTasks = [...tasks].sort((a, b) => a.orderId - b.orderId)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
<DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||||
<SortableContext
|
<SortableContext items={tasks} strategy={verticalListSortingStrategy}>
|
||||||
items={sortedTasks}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
{sortedTasks.map((task, index) => (
|
{topLevelTasks
|
||||||
|
.sort((a, b) => a.orderId - b.orderId)
|
||||||
|
.map((task, index) => (
|
||||||
<SortableItem
|
<SortableItem
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
index={index}
|
index={index}
|
||||||
handleToggle={handleToggle}
|
handleToggle={handleToggle}
|
||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
|
handleAddSubtask={handleAddSubtask}
|
||||||
|
allTasks={tasks}
|
||||||
|
setTasks={setTasks}
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<ListItem sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<ListItem sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Input
|
<Input
|
||||||
placeholder='Add new...'
|
placeholder='Add new task...'
|
||||||
value={newTask}
|
value={newTask}
|
||||||
onChange={e => setNewTask(e.target.value)}
|
onChange={e => setNewTask(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={handleAdd}>
|
<IconButton onClick={handleAdd}>
|
||||||
<Add />
|
<KeyboardReturn />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Add table
Reference in a new issue