feat: Implement multi-select functionality with keyboard shortcuts and bulk actions

This commit is contained in:
Mo Tarbin
2025-06-17 01:00:39 -04:00
parent 19093a4ead
commit 4098f2e498
4 changed files with 873 additions and 11 deletions

View File

@@ -11,6 +11,7 @@ import {
Box,
Button,
Card,
Checkbox,
Chip,
CircularProgress,
Grid,
@@ -47,6 +48,10 @@ const ChoreCard = ({
sx,
viewOnly,
onChipClick,
// Multi-select props
isMultiSelectMode = false,
isSelected = false,
onSelectionToggle,
}) => {
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
React.useState(false)
@@ -392,14 +397,54 @@ const ChoreCard = ({
flexDirection: 'column',
justifyContent: 'space-between',
p: 2,
// backgroundColor: 'white',
boxShadow: 'sm',
borderRadius: 20,
key: `${chore.id}-card`,
// mb: 2,
position: 'relative',
backgroundColor: 'background.surface',
border: '1px solid',
borderColor: 'divider',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: 'md',
borderColor: 'primary.300',
},
// Add padding when in multi-select mode to account for checkbox
pl: isMultiSelectMode ? 6 : 2,
}}
>
{/* Multi-select checkbox */}
{isMultiSelectMode && (
<Checkbox
checked={isSelected}
onChange={onSelectionToggle}
sx={{
position: 'absolute',
top: 12,
left: 12,
zIndex: 2,
bgcolor: 'background.surface',
borderRadius: 'md',
boxShadow: 'sm',
border: '2px solid',
borderColor: 'divider',
'&:hover': {
bgcolor: 'background.level1',
borderColor: 'primary.300',
},
'&.Mui-checked': {
bgcolor: 'primary.500',
borderColor: 'primary.500',
color: 'primary.solidColor',
'&:hover': {
bgcolor: 'primary.600',
borderColor: 'primary.600',
},
},
}}
onClick={e => e.stopPropagation()}
/>
)}
<Grid container>
<Grid
xs={9}

View File

@@ -8,6 +8,7 @@ import {
import {
Box,
Button,
Checkbox,
Chip,
CircularProgress,
IconButton,
@@ -46,6 +47,10 @@ const CompactChoreCard = ({
sx,
viewOnly,
onChipClick,
// Multi-select props
isMultiSelectMode = false,
isSelected = false,
onSelectionToggle,
}) => {
const [isChangeDueDateModalOpen, setIsChangeDueDateModalOpen] =
React.useState(false)
@@ -389,16 +394,17 @@ const CompactChoreCard = ({
...sx,
display: 'flex',
alignItems: 'center',
// px: 1,
// py: 0.75,
minHeight: 56, // More compact height
cursor: 'pointer',
borderBottom: '1px solid',
borderColor: 'divider',
position: 'relative',
pl: '16px', // Add left padding for the priority bar
pl: isMultiSelectMode ? '48px' : '16px', // Add space for checkbox when in multi-select mode
backgroundColor: 'background.surface',
transition: 'all 0.2s ease-in-out',
'&:hover': {
bgcolor: 'background.level1',
boxShadow: 'sm',
},
'&:last-child': {
borderBottom: 'none',
@@ -416,6 +422,37 @@ const CompactChoreCard = ({
}}
onClick={() => navigate(`/chores/${chore.id}`)}
>
{/* Multi-select checkbox */}
{isMultiSelectMode && (
<Checkbox
checked={isSelected}
onChange={onSelectionToggle}
sx={{
position: 'absolute',
left: 16,
zIndex: 2,
bgcolor: 'background.surface',
borderRadius: 'md',
boxShadow: 'sm',
border: '2px solid',
borderColor: 'divider',
'&:hover': {
bgcolor: 'background.level1',
borderColor: 'primary.300',
},
'&.Mui-checked': {
bgcolor: 'primary.500',
borderColor: 'primary.500',
color: 'primary.solidColor',
'&:hover': {
bgcolor: 'primary.600',
borderColor: 'primary.600',
},
},
}}
onClick={e => e.stopPropagation()}
/>
)}
{/* Priority bar clickable area */}
{chore.priority > 0 && (
<Box

View File

@@ -0,0 +1,198 @@
import { Close, HelpOutline, Keyboard } from '@mui/icons-material'
import {
Box,
Button,
Card,
Divider,
IconButton,
Modal,
ModalDialog,
Typography,
} from '@mui/joy'
import { useState } from 'react'
const MultiSelectHelp = ({ isVisible = true }) => {
const [isHelpOpen, setIsHelpOpen] = useState(false)
if (!isVisible) return null
return (
<>
{/* Help Button */}
<IconButton
size='sm'
variant='soft'
color='neutral'
onClick={() => setIsHelpOpen(true)}
sx={{
position: 'fixed',
bottom: 24,
right: 24,
zIndex: 1000,
width: 48,
height: 48,
borderRadius: '50%',
boxShadow: 'lg',
}}
title='Show keyboard shortcuts'
>
<HelpOutline />
</IconButton>
{/* Help Modal */}
<Modal open={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
<ModalDialog
variant='outlined'
size='md'
sx={{
maxWidth: 500,
p: 3,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 2,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Keyboard color='primary' />
<Typography level='title-lg'>Multi-select Mode</Typography>
</Box>
<IconButton
variant='plain'
size='sm'
onClick={() => setIsHelpOpen(false)}
>
<Close />
</IconButton>
</Box>
<Typography level='body-md' sx={{ mb: 3, color: 'text.secondary' }}>
Use these keyboard shortcuts to work more efficiently with multiple
tasks:
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Selection shortcuts */}
<Card variant='soft' sx={{ p: 2 }}>
<Typography
level='title-sm'
sx={{ mb: 1.5, color: 'primary.600' }}
>
Selection
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<ShortcutItem
keys={['Ctrl', 'A']}
description='Select all visible tasks'
/>
<ShortcutItem
keys={['Esc']}
description='Clear selection or exit multi-select mode'
/>
</Box>
</Card>
{/* Action shortcuts */}
<Card variant='soft' sx={{ p: 2 }}>
<Typography
level='title-sm'
sx={{ mb: 1.5, color: 'success.600' }}
>
Actions
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<ShortcutItem
keys={['Enter']}
description='Mark selected tasks as completed'
/>
<ShortcutItem
keys={['Del']}
description='Delete selected tasks'
/>
</Box>
</Card>
{/* Interface shortcuts */}
<Card variant='soft' sx={{ p: 2 }}>
<Typography
level='title-sm'
sx={{ mb: 1.5, color: 'warning.600' }}
>
Interface
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<ShortcutItem
keys={['Ctrl', 'K']}
description='Quick add new task'
/>
</Box>
</Card>
</Box>
<Divider sx={{ my: 3 }} />
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Button
variant='soft'
onClick={() => setIsHelpOpen(false)}
sx={{ minWidth: 120 }}
>
Got it!
</Button>
</Box>
</ModalDialog>
</Modal>
</>
)
}
const ShortcutItem = ({ keys, description }) => (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
}}
>
<Typography level='body-sm' sx={{ flex: 1 }}>
{description}
</Typography>
<Box sx={{ display: 'flex', gap: 0.5 }}>
{keys.map((key, index) => (
<Box
key={index}
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
>
{index > 0 && (
<Typography level='body-xs' color='text.secondary'>
+
</Typography>
)}
<Box
sx={{
px: 1,
py: 0.25,
bgcolor: 'background.level2',
borderRadius: 'sm',
border: '1px solid',
borderColor: 'divider',
minWidth: 32,
textAlign: 'center',
}}
>
<Typography level='body-xs' fontWeight='bold'>
{key}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
)
export default MultiSelectHelp

View File

@@ -2,10 +2,17 @@ import {
Add,
Bolt,
CancelRounded,
CheckBox,
CheckBoxOutlineBlank,
Close,
Delete,
Done,
EditCalendar,
ExpandCircleDown,
Grain,
PriorityHigh,
SelectAll,
SkipNext,
Sort,
Style,
Unarchive,
@@ -37,12 +44,16 @@ import { GetArchivedChores } from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import { useLabels } from '../Labels/LabelQueries'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import ChoreCard from './ChoreCard'
import CompactChoreCard from './CompactChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
import MultiSelectHelp from './MultiSelectHelp'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { ChoreFilters, ChoresGrouper, ChoreSorter } from '../../utils/Chores'
import { DeleteChore, MarkChoreComplete, SkipChore } from '../../utils/Fetcher'
import TaskInput from '../components/AddTaskModal'
import {
canScheduleNotification,
@@ -55,7 +66,8 @@ import SortAndGrouping from './SortAndGrouping'
const MyChores = () => {
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
const { showSuccess } = useNotification()
const { showSuccess, showError } = useNotification()
const { impersonatedUser } = useImpersonateUser()
const [chores, setChores] = useState([])
const [archivedChores, setArchivedChores] = useState(null)
const [filteredChores, setFilteredChores] = useState([])
@@ -92,6 +104,11 @@ const MyChores = () => {
} = useChores()
const { data: membersData, isLoading: membersLoading } = useCircleMembers()
// Multi-select state
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
const [selectedChores, setSelectedChores] = useState(new Set())
const [confirmModelConfig, setConfirmModelConfig] = useState({})
useEffect(() => {
if (!choresLoading && !membersLoading && userProfile) {
setPerformers(membersData.res)
@@ -143,20 +160,133 @@ const MyChores = () => {
}
}, [searchInputFocus])
// add listern to Control/Command + K to focus on search input
// Keyboard shortcuts for multi-select and other actions
useEffect(() => {
const handleKeyDown = event => {
// Ctrl/Cmd + K to open task modal
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
setAddTaskModalOpen(true)
return
}
}
document.addEventListener('keydown', handleKeyDown)
// Ctrl/Cmd + A to select all - works both in and out of multi-select mode
if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
event.preventDefault()
if (!isMultiSelectMode) {
// Enable multi-select mode and select all visible tasks
setIsMultiSelectMode(true)
setTimeout(() => {
selectAllVisibleChores()
}, 0)
// showSuccess({
// title: '🎯 Multi-select Mode Active',
// message: 'Selected all visible tasks. Press Esc to exit.',
// })
} else {
// Already in multi-select mode, check if all visible tasks are already selected
let visibleChores = []
if (searchTerm?.length > 0 || searchFilter !== 'All') {
visibleChores = filteredChores
const allVisibleSelected =
visibleChores.length > 0 &&
visibleChores.every(chore => selectedChores.has(chore.id))
if (allVisibleSelected) {
showSuccess({
title: '✅ All Tasks Selected',
message: `All ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's are' : ' is'} already selected.`,
})
} else {
selectAllVisibleChores()
showSuccess({
title: '🎯 Tasks Selected',
message: `Selected ${visibleChores.length} filtered task${visibleChores.length !== 1 ? 's' : ''}.`,
})
}
} else {
// Check expanded sections first
const expandedChores = choreSections
.filter((section, index) => openChoreSections[index])
.flatMap(section => section.content || [])
const allExpandedSelected =
expandedChores.length > 0 &&
expandedChores.every(chore => selectedChores.has(chore.id))
// Get all chores (including collapsed sections)
const allChores = choreSections.flatMap(
section => section.content || [],
)
const allChoresSelected =
allChores.length > 0 &&
allChores.every(chore => selectedChores.has(chore.id))
if (allChoresSelected) {
// All chores (including collapsed) are already selected
showSuccess({
title: '✅ All Tasks Selected',
message: `All ${allChores.length} task${allChores.length !== 1 ? 's are' : ' is'} already selected (including collapsed sections).`,
})
} else if (allExpandedSelected) {
// All expanded are selected, now select ALL (including collapsed)
selectAllVisibleChores() // This will now select all chores
const collapsedCount = allChores.length - expandedChores.length
showSuccess({
title: '🎯 All Tasks Selected',
message: `Selected all ${allChores.length} tasks (including ${collapsedCount} from collapsed sections).`,
})
} else {
// Not all expanded are selected, select expanded only
selectAllVisibleChores() // This will select expanded only
showSuccess({
title: '🎯 Tasks Selected',
message: `Selected ${expandedChores.length} task${expandedChores.length !== 1 ? 's' : ''} from expanded sections.`,
})
}
}
}
return
}
// Multi-select keyboard shortcuts (only when in multi-select mode)
if (isMultiSelectMode) {
// Escape to clear selection or exit multi-select mode
if (event.key === 'Escape') {
event.preventDefault()
if (selectedChores.size > 0) {
clearSelection()
} else {
setIsMultiSelectMode(false)
}
return
}
// Delete/Backspace key for bulk delete (with confirmation)
if (
(event.key === 'Delete' || event.key === 'Backspace') &&
selectedChores.size > 0
) {
event.preventDefault()
handleBulkDelete()
return
}
// Enter key for bulk complete
if (event.key === 'Enter' && selectedChores.size > 0) {
event.preventDefault()
handleBulkComplete()
return
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [])
}, [isMultiSelectMode, selectedChores.size])
const setSelectedChoreSectionWithCache = value => {
setSelectedChoreSection(value)
localStorage.setItem('selectedChoreSection', value)
@@ -188,6 +318,10 @@ const MyChores = () => {
performers={performers}
userLabels={userLabels}
onChipClick={handleLabelFiltering}
// Multi-select props
isMultiSelectMode={isMultiSelectMode}
isSelected={selectedChores.has(chore.id)}
onSelectionToggle={() => toggleChoreSelection(chore.id)}
/>
)
}
@@ -375,6 +509,246 @@ const MyChores = () => {
setFilteredChores(fuse.search(term).map(result => result.item))
}
// Multi-select helper functions
const toggleMultiSelectMode = () => {
const newMode = !isMultiSelectMode
setIsMultiSelectMode(newMode)
if (newMode) {
setSelectedChores(new Set()) // Clear selection when exiting multi-select
}
}
const toggleChoreSelection = choreId => {
const newSelection = new Set(selectedChores)
if (newSelection.has(choreId)) {
newSelection.delete(choreId)
} else {
newSelection.add(choreId)
}
setSelectedChores(newSelection)
}
const selectAllVisibleChores = () => {
let visibleChores = []
if (searchTerm?.length > 0 || searchFilter !== 'All') {
// If there's a search term or filter, all filtered chores are visible
visibleChores = filteredChores
} else {
// First, get chores from expanded sections only
const expandedChores = choreSections
.filter((section, index) => openChoreSections[index]) // Only expanded sections
.flatMap(section => section.content || []) // Get all chores from expanded sections
// Check if all expanded chores are already selected
const allExpandedSelected =
expandedChores.length > 0 &&
expandedChores.every(chore => selectedChores.has(chore.id))
if (allExpandedSelected) {
// If all expanded chores are already selected, select ALL chores (including collapsed sections)
visibleChores = choreSections.flatMap(section => section.content || [])
} else {
// Otherwise, just select expanded chores
visibleChores = expandedChores
}
}
if (visibleChores.length > 0) {
const allIds = new Set(visibleChores.map(chore => chore.id))
setSelectedChores(allIds)
}
}
const clearSelection = () => {
// if already empty, just exit multi-select mode:
if (selectedChores.size === 0) {
setIsMultiSelectMode(false)
return
}
setSelectedChores(new Set())
}
const getSelectedChoresData = () => {
const allChores = [...chores, ...(archivedChores || [])]
return Array.from(selectedChores)
.map(id => allChores.find(chore => chore.id === id))
.filter(Boolean)
}
// Bulk operations with improved UX and confirmation modal
const handleBulkComplete = async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Complete Tasks',
confirmText: 'Complete',
cancelText: 'Cancel',
message: `Mark ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} as completed?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const completedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await MarkChoreComplete(
chore.id,
impersonatedUser
? { completedBy: impersonatedUser.userId }
: null,
null,
null,
)
completedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (completedTasks.length > 0) {
showSuccess({
title: '✅ Tasks Completed',
message: `Successfully completed ${completedTasks.length} task${completedTasks.length > 1 ? 's' : ''}.`,
})
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be completed.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Complete Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}
const handleBulkDelete = async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Delete Tasks',
confirmText: 'Delete',
cancelText: 'Cancel',
message: `Delete ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const deletedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await DeleteChore(chore.id)
deletedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (deletedTasks.length > 0) {
showSuccess({
title: '🗑️ Tasks Deleted',
message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
})
const deletedIds = new Set(deletedTasks.map(c => c.id))
setChores(chores.filter(c => !deletedIds.has(c.id)))
setFilteredChores(
filteredChores.filter(c => !deletedIds.has(c.id)),
)
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
})
}
clearSelection()
} catch (error) {
showError({
title: 'Bulk Delete Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}
const handleBulkSkip = async () => {
const selectedData = getSelectedChoresData()
if (selectedData.length === 0) return
setConfirmModelConfig({
isOpen: true,
title: 'Skip Tasks',
confirmText: 'Skip',
cancelText: 'Cancel',
message: `Skip ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to next due date?`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
const skippedTasks = []
const failedTasks = []
for (const chore of selectedData) {
try {
await SkipChore(chore.id)
skippedTasks.push(chore)
} catch (error) {
failedTasks.push(chore)
}
}
if (skippedTasks.length > 0) {
showSuccess({
title: '⏭️ Tasks Skipped',
message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
})
}
if (failedTasks.length > 0) {
showError({
title: 'Some Tasks Failed',
message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be skipped.`,
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
title: 'Bulk Skip Failed',
message: 'An unexpected error occurred. Please try again.',
})
}
}
setConfirmModelConfig({})
},
})
}
if (
isUserProfileLoading ||
userLabelsLoading ||
@@ -536,6 +910,26 @@ const MyChores = () => {
>
{isCompactView ? <ViewModule /> : <ViewAgenda />}
</IconButton>
{/* Multi-select Toggle Button */}
<IconButton
variant={isMultiSelectMode ? 'solid' : 'outlined'}
color={isMultiSelectMode ? 'primary' : 'neutral'}
size='sm'
sx={{
height: 32,
width: 32,
borderRadius: '50%',
}}
onClick={toggleMultiSelectMode}
title={
isMultiSelectMode
? 'Exit Multi-select Mode'
: 'Enable Multi-select Mode'
}
>
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
</IconButton>
</Box>
{showSearchFilter && (
<div className='flex gap-4'>
@@ -657,6 +1051,186 @@ const MyChores = () => {
</IconButton>
</div>
)}
{/* Multi-select Toolbar */}
{isMultiSelectMode && (
<Box
sx={{
borderRadius: 'lg',
p: 2,
mb: 2,
border: '1px solid',
borderColor: 'divider',
// boxShadow: 'sm',
gap: 2,
display: 'flex',
flexDirection: {
xs: 'column', // Stack vertically on mobile
sm: 'row', // Horizontal on tablet and larger
},
alignItems: {
xs: 'stretch', // Full width on mobile
sm: 'center', // Center aligned on larger screens
},
justifyContent: {
xs: 'center',
sm: 'space-between',
},
}}
>
{/* Selection Info and Controls */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
flexWrap: {
xs: 'wrap', // Allow wrapping on mobile if needed
sm: 'nowrap',
},
justifyContent: {
xs: 'center', // Center on mobile
sm: 'flex-start',
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckBox sx={{ color: 'primary.500' }} />
<Typography level='body-sm' fontWeight='md'>
{selectedChores.size} task
{selectedChores.size !== 1 ? 's' : ''} selected
</Typography>
</Box>
<Divider
orientation='vertical'
sx={{
display: { xs: 'none', sm: 'block' }, // Hide vertical divider on mobile
}}
/>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size='sm'
variant='outlined'
onClick={selectAllVisibleChores}
startDecorator={<SelectAll />}
disabled={
searchTerm?.length > 0 || searchFilter !== 'All'
? selectedChores.size === filteredChores.length
: selectedChores.size ===
choreSections.flatMap(s => s.content || []).length
}
sx={{
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
}}
>
All
</Button>
<Button
size='sm'
variant='outlined'
onClick={clearSelection}
startDecorator={
selectedChores.size === 0 ? (
<Close />
) : (
<CheckBoxOutlineBlank />
)
}
sx={{
minWidth: 'auto',
'--Button-paddingInline': '0.75rem',
}}
>
{selectedChores.size === 0 ? 'Close' : 'Clear'}
</Button>
</Box>
</Box>
{/* Action Buttons */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: {
xs: 'wrap', // Allow wrapping on mobile
sm: 'nowrap',
},
justifyContent: {
xs: 'center', // Center on mobile
sm: 'flex-end',
},
}}
>
<Button
size='sm'
variant='solid'
color='success'
onClick={handleBulkComplete}
startDecorator={<Done />}
disabled={selectedChores.size === 0}
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
>
Complete
</Button>
<Button
size='sm'
variant='soft'
color='warning'
onClick={handleBulkSkip}
startDecorator={<SkipNext />}
disabled={selectedChores.size === 0}
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
>
Skip
</Button>
<Button
size='sm'
variant='soft'
color='danger'
onClick={handleBulkDelete}
startDecorator={<Delete />}
disabled={selectedChores.size === 0}
sx={{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
}}
>
Delete
</Button>
{/*
<Divider
orientation='vertical'
sx={{
display: { xs: 'none', sm: 'block' }, // Hide vertical divider on mobile
}}
/>
<IconButton
size='sm'
variant='plain'
onClick={toggleMultiSelectMode}
color='neutral'
title='Exit multi-select mode (Esc)'
sx={{
'&:hover': {
bgcolor: 'danger.softBg',
color: 'danger.softColor',
},
}}
>
<CancelRounded />
</IconButton> */}
</Box>
</Box>
)}
{searchFilter !== 'All' && (
<Chip
level='title-md'
@@ -902,6 +1476,14 @@ const MyChores = () => {
</Container>
<Sidepanel chores={chores} performers={performers} />
{/* Multi-select Help - only show when in multi-select mode */}
<MultiSelectHelp isVisible={isMultiSelectMode} />
{/* Confirmation Modal for bulk operations */}
{confirmModelConfig?.isOpen && (
<ConfirmationModal config={confirmModelConfig} />
)}
</div>
)
}
@@ -949,7 +1531,7 @@ const FILTERS = {
return chore.assignedTo === userID
})
},
'No Due Date': function (chores, userID) {
'No Due Date': function (chores) {
return chores.filter(chore => {
return chore.nextDueDate === null
})