implement unified ChoreToolbar and enhance ChoreHistory with filtering options
This commit is contained in:
@@ -2,15 +2,10 @@ import {
|
||||
Add,
|
||||
Bolt,
|
||||
CalendarMonth,
|
||||
CheckBox,
|
||||
CheckBoxOutlineBlank,
|
||||
EditCalendar,
|
||||
ExpandCircleDown,
|
||||
PriorityHigh,
|
||||
Sort,
|
||||
Style,
|
||||
ViewAgenda,
|
||||
ViewModule,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Accordion,
|
||||
@@ -37,7 +32,6 @@ import IconButtonWithMenu from './IconButtonWithMenu'
|
||||
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import FilterBar from '../../components/common/FilterBar'
|
||||
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useFilter } from '../../hooks/useFilter'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
@@ -52,15 +46,13 @@ import { getSafeBottom } from '../../utils/SafeAreaUtils.js'
|
||||
import TaskInput from '../components/AddTaskModal'
|
||||
import CalendarDual from '../components/CalendarDual'
|
||||
import CalendarMonthly from '../components/CalendarMonthly.jsx'
|
||||
import ProjectSelector from '../components/ProjectSelector'
|
||||
import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
|
||||
import { useProjects } from '../Projects/ProjectQueries.js'
|
||||
import ChoreListView from './ChoreListView.jsx'
|
||||
import ChoreToolbar from './components/ChoreToolbarPrototype'
|
||||
import ChoreModals from './components/ChoreModals'
|
||||
import FilterSection from './components/FilterSection'
|
||||
import MultiSelectToolbar from './components/MultiSelectToolbar'
|
||||
import MyChoreHeader from './components/MyChoreHeader'
|
||||
import SearchBar from './components/SearchBar'
|
||||
import { useChoreActions } from './hooks/useChoreActions'
|
||||
import { useChoreFilters } from './hooks/useChoreFilters'
|
||||
import { useChoreModals } from './hooks/useChoreModals'
|
||||
@@ -75,7 +67,6 @@ import {
|
||||
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
|
||||
import Sidepanel from './Sidepanel'
|
||||
import { INSIGHT_FILTER_DEFS } from './SmartInsightsCard'
|
||||
import SortAndGrouping from './SortAndGrouping'
|
||||
|
||||
const MyChores = () => {
|
||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||
@@ -790,14 +781,13 @@ const MyChores = () => {
|
||||
localStorage.setItem('openChoreSections', JSON.stringify(value))
|
||||
}
|
||||
|
||||
const toggleViewMode = () => {
|
||||
const modes = ['default', 'compact', 'calendar']
|
||||
const currentIndex = modes.indexOf(viewMode)
|
||||
const nextIndex = (currentIndex + 1) % modes.length
|
||||
const newMode = modes[nextIndex]
|
||||
const toggleViewMode = value => {
|
||||
const newMode = value ?? (() => {
|
||||
const modes = ['default', 'compact', 'calendar']
|
||||
return modes[(modes.indexOf(viewMode) + 1) % modes.length]
|
||||
})()
|
||||
setViewMode(newMode)
|
||||
localStorage.setItem('choreCardViewMode', newMode)
|
||||
|
||||
if (newMode !== 'calendar') {
|
||||
setSelectedCalendarDate(null)
|
||||
}
|
||||
@@ -957,126 +947,7 @@ const MyChores = () => {
|
||||
tempFilter={tempFilter}
|
||||
tempFilterMeta={tempFilterMeta}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
<SearchBar
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
onClose={handleSearchClose}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
|
||||
<SortAndGrouping
|
||||
title='Group by'
|
||||
k={'icon-menu-group-by'}
|
||||
icon={<Sort />}
|
||||
selectedItem={selectedChoreSection}
|
||||
selectedFilter={selectedChoreFilter}
|
||||
setFilter={filter => {
|
||||
setSelectedChoreFilterWithCache(filter)
|
||||
// Clear active custom filter when quick filter is applied
|
||||
if (activeFilterId) {
|
||||
clearActiveFilter()
|
||||
updateFilterUrl(null, null)
|
||||
}
|
||||
}}
|
||||
onItemSelect={selected => {
|
||||
setSelectedChoreSectionWithCache(selected.value)
|
||||
setFilteredChores(chores)
|
||||
clearQuickFilters()
|
||||
}}
|
||||
onCreateNewFilter={() => {
|
||||
setShowAdvancedFilterBuilder(true)
|
||||
setEditingFilter(null)
|
||||
}}
|
||||
mouseClickHandler={handleMenuOutsideClick}
|
||||
/>
|
||||
|
||||
{/* Project Selector - Hidden when active filter has project conditions */}
|
||||
{projectsWithDefault.length > 1 &&
|
||||
!hasProjectConditions &&
|
||||
!hasFilterApplied && (
|
||||
<ProjectSelector
|
||||
selectedProject={selectedProject?.name || 'Default Project'}
|
||||
onProjectSelect={project => {
|
||||
setSelectedProjectWithCache(project)
|
||||
clearActiveFilter()
|
||||
}}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* View Mode Toggle Button */}
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='sm'
|
||||
sx={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
onClick={toggleViewMode}
|
||||
title={
|
||||
viewMode === 'default'
|
||||
? 'Switch to Compact View'
|
||||
: viewMode === 'compact'
|
||||
? 'Switch to Calendar View'
|
||||
: 'Switch to Card View'
|
||||
}
|
||||
>
|
||||
{viewMode === 'default' ? (
|
||||
<ViewAgenda />
|
||||
) : viewMode === 'compact' ? (
|
||||
<CalendarMonth />
|
||||
) : (
|
||||
<ViewModule />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
{/* Multi-select Toggle Button */}
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<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 (Ctrl+S)'
|
||||
: 'Enable Multi-select Mode (Ctrl+S)'
|
||||
}
|
||||
>
|
||||
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
|
||||
</IconButton>
|
||||
<KeyboardShortcutHint
|
||||
shortcut='S'
|
||||
show={showKeyboardShortcuts}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -8,
|
||||
right: -8,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Quick Filters */}
|
||||
<FilterBar
|
||||
<ChoreToolbar
|
||||
filterDefs={quickFilterDefs}
|
||||
activeFilters={quickFilters}
|
||||
onSetFilter={(id, value) => {
|
||||
@@ -1084,21 +955,40 @@ const MyChores = () => {
|
||||
setQuickFilter(id, value)
|
||||
setSelectedCalendarDate(null)
|
||||
}}
|
||||
onClearAll={() => {
|
||||
onClearAllFilters={() => {
|
||||
clearQuickFilters()
|
||||
clearActiveFilter()
|
||||
setSelectedChoreFilterWithCache('anyone')
|
||||
setSelectedProjectWithCache(projectsWithDefault.find(p => p.id === 'default') || null)
|
||||
updateFilterUrl(null, null)
|
||||
}}
|
||||
resultCount={hasQuickFilters ? getFilteredChores.length : undefined}
|
||||
totalCount={hasQuickFilters ? projectFilteredChores.length : undefined}
|
||||
/>
|
||||
|
||||
{/* Custom Filters Section */}
|
||||
<FilterSection
|
||||
resultCount={
|
||||
hasQuickFilters || !!activeFilterId ? getFilteredChores.length : undefined
|
||||
}
|
||||
totalCount={
|
||||
hasQuickFilters || !!activeFilterId ? projectFilteredChores.length : undefined
|
||||
}
|
||||
projects={
|
||||
!hasProjectConditions && !hasFilterApplied
|
||||
? projectsWithDefault
|
||||
: []
|
||||
}
|
||||
selectedProject={selectedProject}
|
||||
onProjectSelect={project => {
|
||||
setSelectedProjectWithCache(project)
|
||||
clearActiveFilter()
|
||||
}}
|
||||
selectedAssigneeFilter={selectedChoreFilter}
|
||||
onAssigneeFilterChange={filter => {
|
||||
setSelectedChoreFilterWithCache(filter)
|
||||
if (activeFilterId) {
|
||||
clearActiveFilter()
|
||||
updateFilterUrl(null, null)
|
||||
}
|
||||
}}
|
||||
savedFilters={savedFilters}
|
||||
activeFilterId={activeFilterId}
|
||||
activeFilter={activeFilter}
|
||||
hasProjectConditions={hasProjectConditions}
|
||||
onFilterClick={filterId => {
|
||||
onSavedFilterClick={filterId => {
|
||||
if (activeFilterId === filterId) {
|
||||
clearActiveFilter()
|
||||
updateFilterUrl(null, null)
|
||||
@@ -1106,31 +996,42 @@ const MyChores = () => {
|
||||
clearQuickFilters()
|
||||
setSearchTerm('')
|
||||
setFilteredChores([])
|
||||
|
||||
// Reset quick filter to 'anyone' when custom filter is applied
|
||||
if (selectedChoreFilter !== 'anyone') {
|
||||
setSelectedChoreFilterWithCache('anyone')
|
||||
}
|
||||
|
||||
// Clear project selection if the filter has project conditions
|
||||
const filter = savedFilters.find(f => f.id === filterId)
|
||||
if (filter?.conditions?.some(c => c.type === 'project')) {
|
||||
setSelectedProjectWithCache(null)
|
||||
}
|
||||
|
||||
applyCustomFilter(filterId)
|
||||
updateFilterUrl('filterId', filterId)
|
||||
}
|
||||
}}
|
||||
onFilterDelete={deleteFilter}
|
||||
onFilterPin={pinFilter}
|
||||
onFilterEdit={filter => {
|
||||
onSavedFilterEdit={filter => {
|
||||
setEditingFilter(filter)
|
||||
setShowAdvancedFilterBuilder(true)
|
||||
}}
|
||||
onClearActiveFilter={clearActiveFilter}
|
||||
onCreateAdvancedFilter={() => setShowAdvancedFilterBuilder(true)}
|
||||
updateFilterUrl={updateFilterUrl}
|
||||
onSavedFilterDelete={deleteFilter}
|
||||
onSavedFilterPin={pinFilter}
|
||||
onCreateAdvancedFilter={() => {
|
||||
setShowAdvancedFilterBuilder(true)
|
||||
setEditingFilter(null)
|
||||
}}
|
||||
selectedGroupBy={selectedChoreSection}
|
||||
onGroupBySelect={value => {
|
||||
setSelectedChoreSectionWithCache(value)
|
||||
setFilteredChores(chores)
|
||||
clearQuickFilters()
|
||||
}}
|
||||
viewMode={viewMode}
|
||||
onToggleViewMode={toggleViewMode}
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
onToggleMultiSelect={toggleMultiSelectMode}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={handleSearchChange}
|
||||
onSearchClose={handleSearchClose}
|
||||
searchInputRef={searchInputRef}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
/>
|
||||
|
||||
<MultiSelectToolbar
|
||||
|
||||
732
src/views/Chores/components/ChoreToolbarPrototype.jsx
Normal file
732
src/views/Chores/components/ChoreToolbarPrototype.jsx
Normal file
@@ -0,0 +1,732 @@
|
||||
/**
|
||||
* PROTOTYPE – Unified Chore Toolbar
|
||||
*
|
||||
* Proposed design to replace the current 3-surface layout:
|
||||
* OLD: [Search] [Sort+Group+AssigneeFilter+CreateFilter] [ProjectSelector] [View] [Multiselect]
|
||||
* + FilterBar (Due Date / Priority / Labels chips row)
|
||||
* + FilterSection (saved/pinned filter chips row)
|
||||
*
|
||||
* NEW: [Search] [Filter(n)] [Group ▾] [View] [Multiselect]
|
||||
* + active filter chips appear inline next to Filter button
|
||||
* + Filter button opens ONE unified bottom sheet containing:
|
||||
* Project · Assignee · Due Date · Priority · Labels · Saved Filters
|
||||
*
|
||||
* How to try it: in MyChores.jsx, replace the <Box sx={{display:'flex'...}}> toolbar block
|
||||
* and the two rows below it (FilterBar + FilterSection) with:
|
||||
* <ChoreToolbar ... />
|
||||
*/
|
||||
|
||||
import {
|
||||
Add,
|
||||
CalendarMonth,
|
||||
Check,
|
||||
CheckBox,
|
||||
CheckBoxOutlineBlank,
|
||||
Close,
|
||||
FilterList,
|
||||
FolderOpen,
|
||||
PriorityHigh,
|
||||
Settings,
|
||||
Sort,
|
||||
Style,
|
||||
Tune,
|
||||
ViewAgenda,
|
||||
ViewComfy,
|
||||
ViewModule,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Divider,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import BottomSheetModal from '../../../components/common/BottomSheetModal'
|
||||
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
||||
import SearchBar from './SearchBar'
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const chipLabel = (def, value) => {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) return null
|
||||
if (def.type === 'single-select')
|
||||
return def.options?.find(o => o.value === value)?.label ?? null
|
||||
if (def.type === 'multi-select' && Array.isArray(value)) {
|
||||
if (value.length === 1)
|
||||
return def.options?.find(o => o.value === value[0])?.label ?? def.label
|
||||
return `${def.label} (${value.length})`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ─── sub-components ──────────────────────────────────────────────────────────
|
||||
|
||||
const SectionHeader = ({ icon, label, badge }) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
{icon && (
|
||||
<Box
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& svg': { fontSize: 18 },
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
)}
|
||||
<Typography level='title-sm' fontWeight={600}>
|
||||
{label}
|
||||
</Typography>
|
||||
{badge != null && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='solid'
|
||||
color='primary'
|
||||
sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}
|
||||
>
|
||||
{badge}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
const OptionChips = ({ options, selected, multi, onToggle }) => (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{options.map(opt => {
|
||||
const isSelected = multi
|
||||
? (selected || []).includes(opt.value)
|
||||
: selected === opt.value
|
||||
return (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
variant={isSelected ? 'solid' : 'soft'}
|
||||
color={isSelected ? opt.color ?? 'primary' : 'neutral'}
|
||||
startDecorator={
|
||||
opt.icon != null
|
||||
? isSelected
|
||||
? <Check sx={{ fontSize: 14 }} />
|
||||
: opt.icon
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onToggle(opt.value)}
|
||||
sx={{
|
||||
py: 0.64,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': { opacity: 0.85 },
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ─── main component ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* filterDefs – same shape as FilterBar: [{ id, label, type, icon, options, filterFn }]
|
||||
* PLUS two new built-in sections handled here: 'project' and 'assignee'
|
||||
* activeFilters – { [id]: value }
|
||||
* onSetFilter – (id, value | null) => void
|
||||
* onClearAllFilters – () => void
|
||||
* resultCount / totalCount
|
||||
*
|
||||
* projects – [{ id, name }] — drives the Project section
|
||||
* selectedProject – current project object
|
||||
* onProjectSelect – (project) => void
|
||||
*
|
||||
* selectedAssigneeFilter – 'anyone' | 'assigned_to_me' | 'available_for_me' | 'assigned_to_others'
|
||||
* onAssigneeFilterChange – (key) => void
|
||||
*
|
||||
* savedFilters – [{ id, name, color, count, isPinned }]
|
||||
* activeFilterId – number | null
|
||||
* onSavedFilterClick – (id) => void
|
||||
* onSavedFilterEdit – (filter) => void
|
||||
* onSavedFilterDelete– (id) => void
|
||||
* onSavedFilterPin – (id) => void
|
||||
* onCreateAdvancedFilter – () => void
|
||||
*
|
||||
* selectedGroupBy – 'default' | 'due_date' | 'priority' | 'labels'
|
||||
* onGroupBySelect – (value) => void
|
||||
*
|
||||
* viewMode – 'default' | 'compact' | 'calendar'
|
||||
* onToggleViewMode – () => void
|
||||
*
|
||||
* isMultiSelectMode – bool
|
||||
* onToggleMultiSelect – () => void
|
||||
*
|
||||
* searchTerm / onSearchChange / onSearchClose / searchInputRef
|
||||
* showKeyboardShortcuts
|
||||
*/
|
||||
const ChoreToolbar = ({
|
||||
// quick filters
|
||||
filterDefs = [],
|
||||
activeFilters = {},
|
||||
onSetFilter,
|
||||
onClearAllFilters,
|
||||
resultCount,
|
||||
totalCount,
|
||||
// project
|
||||
projects = [],
|
||||
selectedProject,
|
||||
onProjectSelect,
|
||||
// assignee
|
||||
selectedAssigneeFilter = 'anyone',
|
||||
onAssigneeFilterChange,
|
||||
// saved / custom
|
||||
savedFilters = [],
|
||||
activeFilterId,
|
||||
onSavedFilterClick,
|
||||
onSavedFilterEdit,
|
||||
onSavedFilterDelete,
|
||||
onSavedFilterPin,
|
||||
onCreateAdvancedFilter,
|
||||
// grouping
|
||||
selectedGroupBy = 'default',
|
||||
onGroupBySelect,
|
||||
// view + multiselect
|
||||
viewMode = 'default',
|
||||
onToggleViewMode,
|
||||
isMultiSelectMode,
|
||||
onToggleMultiSelect,
|
||||
// search
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
onSearchClose,
|
||||
searchInputRef,
|
||||
showKeyboardShortcuts,
|
||||
}) => {
|
||||
const [filterSheetOpen, setFilterSheetOpen] = useState(false)
|
||||
const [displaySheetOpen, setDisplaySheetOpen] = useState(false)
|
||||
|
||||
// ── count active filters for badge ──────────────────────────────────────────
|
||||
|
||||
const quickFilterCount = filterDefs.filter(def => {
|
||||
const v = activeFilters[def.id]
|
||||
return v != null && !(Array.isArray(v) && v.length === 0)
|
||||
}).length
|
||||
|
||||
const projectActive =
|
||||
selectedProject && selectedProject.id !== 'default' ? 1 : 0
|
||||
const assigneeActive = selectedAssigneeFilter !== 'anyone' ? 1 : 0
|
||||
const savedFilterActive = activeFilterId != null ? 1 : 0
|
||||
|
||||
const totalActiveCount = quickFilterCount + savedFilterActive
|
||||
const hasAnyActive = totalActiveCount > 0
|
||||
|
||||
// ── inline chip strip (max 2 visible + overflow) ─────────────────────────────
|
||||
|
||||
const inlineChips = []
|
||||
|
||||
if (savedFilterActive) {
|
||||
const sf = savedFilters.find(f => f.id === activeFilterId)
|
||||
if (sf)
|
||||
inlineChips.push({
|
||||
key: '__saved',
|
||||
label: sf.name,
|
||||
onClear: () => onSavedFilterClick?.(activeFilterId),
|
||||
})
|
||||
}
|
||||
|
||||
filterDefs.forEach(def => {
|
||||
const label = chipLabel(def, activeFilters[def.id])
|
||||
if (label) inlineChips.push({ key: def.id, label, onClear: () => onSetFilter(def.id, null) })
|
||||
})
|
||||
|
||||
const MAX_CHIPS = 2
|
||||
const visibleChips = inlineChips.slice(0, MAX_CHIPS)
|
||||
const overflow = inlineChips.length - MAX_CHIPS
|
||||
|
||||
// ── groupby options ──────────────────────────────────────────────────────────
|
||||
|
||||
const groupByOptions = [
|
||||
{ value: 'default', label: 'Smart' },
|
||||
{ value: 'due_date', label: 'Due Date' },
|
||||
{ value: 'priority', label: 'Priority' },
|
||||
{ value: 'labels', label: 'Labels' },
|
||||
]
|
||||
|
||||
// ── assignee options ─────────────────────────────────────────────────────────
|
||||
|
||||
const assigneeOptions = [
|
||||
{ value: 'anyone', label: 'Everyone' },
|
||||
{ value: 'assigned_to_me', label: 'Mine' },
|
||||
{ value: 'available_for_me', label: 'Available to me' },
|
||||
{ value: 'assigned_to_others', label: 'Others' },
|
||||
]
|
||||
|
||||
// ── project options (show only if more than just Default) ───────────────────
|
||||
|
||||
const showProjectSection = projects.filter(p => p.id !== 'default').length > 0
|
||||
|
||||
// ── pinned saved filters ─────────────────────────────────────────────────────
|
||||
|
||||
const pinnedFilters = savedFilters.filter(f => f.isPinned)
|
||||
|
||||
// ── display active state (highlight button when non-default) ─────────────────
|
||||
|
||||
const displayActive =
|
||||
selectedGroupBy !== 'default' ||
|
||||
viewMode !== 'default' ||
|
||||
projectActive > 0 ||
|
||||
assigneeActive > 0
|
||||
|
||||
const viewOptions = [
|
||||
{ value: 'default', label: 'Cards', icon: <ViewAgenda sx={{ fontSize: 16 }} /> },
|
||||
{ value: 'compact', label: 'Compact', icon: <ViewComfy sx={{ fontSize: 16 }} /> },
|
||||
{ value: 'calendar', label: 'Calendar', icon: <CalendarMonth sx={{ fontSize: 16 }} /> },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Row 1: main toolbar ─────────────────────────────────────────────── */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Search takes available space */}
|
||||
<SearchBar
|
||||
value={searchTerm}
|
||||
onChange={onSearchChange}
|
||||
onClose={onSearchClose}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
inputRef={searchInputRef}
|
||||
/>
|
||||
|
||||
{/* Filter button */}
|
||||
<Badge
|
||||
badgeContent={totalActiveCount || null}
|
||||
color='primary'
|
||||
size='sm'
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<IconButton
|
||||
variant={hasAnyActive ? 'solid' : 'outlined'}
|
||||
color={hasAnyActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
sx={{ height: 32, width: 32, borderRadius: '50%' }}
|
||||
onClick={() => setFilterSheetOpen(true)}
|
||||
title='Filters'
|
||||
>
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
</Badge>
|
||||
|
||||
{/* Display button — View + Group combined */}
|
||||
<IconButton
|
||||
variant={displayActive ? 'solid' : 'outlined'}
|
||||
color={displayActive ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
sx={{ height: 32, width: 32, borderRadius: '50%' }}
|
||||
onClick={() => setDisplaySheetOpen(true)}
|
||||
title='View & Group'
|
||||
>
|
||||
{viewMode === 'calendar' ? (
|
||||
<CalendarMonth />
|
||||
) : viewMode === 'compact' ? (
|
||||
<ViewModule />
|
||||
) : (
|
||||
<ViewAgenda />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
{/* Multiselect */}
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<IconButton
|
||||
variant={isMultiSelectMode ? 'solid' : 'outlined'}
|
||||
color={isMultiSelectMode ? 'primary' : 'neutral'}
|
||||
size='sm'
|
||||
sx={{ height: 32, width: 32, borderRadius: '50%' }}
|
||||
onClick={onToggleMultiSelect}
|
||||
title={isMultiSelectMode ? 'Exit multi-select (Ctrl+S)' : 'Multi-select (Ctrl+S)'}
|
||||
>
|
||||
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
|
||||
</IconButton>
|
||||
<KeyboardShortcutHint
|
||||
shortcut='S'
|
||||
show={showKeyboardShortcuts}
|
||||
sx={{ position: 'absolute', top: -8, right: -8, zIndex: 1000 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Row 2: active filter chips (only when something is active) ─────── */}
|
||||
{hasAnyActive && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
py: 0.5,
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
}}
|
||||
>
|
||||
{visibleChips.map(({ key, label, onClear }) => (
|
||||
<Chip
|
||||
key={key}
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='primary'
|
||||
endDecorator={
|
||||
<Close
|
||||
sx={{ fontSize: 12, cursor: 'pointer' }}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onClear()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => setFilterSheetOpen(true)}
|
||||
sx={{ cursor: 'pointer', flexShrink: 0 }}
|
||||
>
|
||||
{label}
|
||||
</Chip>
|
||||
))}
|
||||
|
||||
{overflow > 0 && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={() => setFilterSheetOpen(true)}
|
||||
sx={{ cursor: 'pointer', flexShrink: 0 }}
|
||||
>
|
||||
+{overflow} more
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{resultCount != null && totalCount != null && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.tertiary', ml: 'auto', flexShrink: 0 }}
|
||||
>
|
||||
{resultCount} / {totalCount}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
sx={{
|
||||
px: 0.5,
|
||||
fontSize: '0.72rem',
|
||||
color: 'text.tertiary',
|
||||
minHeight: 0,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onClick={onClearAllFilters}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── Unified Filter bottom sheet ─────────────────────────────────────── */}
|
||||
<BottomSheetModal
|
||||
open={filterSheetOpen}
|
||||
onClose={() => setFilterSheetOpen(false)}
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tune sx={{ fontSize: 20 }} />
|
||||
Filters
|
||||
{hasAnyActive && (
|
||||
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 0.5 }}>
|
||||
{totalActiveCount}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
footer={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='danger'
|
||||
size='sm'
|
||||
disabled={!hasAnyActive}
|
||||
onClick={onClearAllFilters}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setFilterSheetOpen(false)}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
{resultCount != null
|
||||
? `Show ${resultCount} result${resultCount !== 1 ? 's' : ''}`
|
||||
: 'Done'}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
|
||||
{/* ── Quick filter sections (Due Date / Priority / Labels etc.) ────── */}
|
||||
{filterDefs.map(def => (
|
||||
<Box key={def.id}>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<SectionHeader
|
||||
icon={def.icon}
|
||||
label={def.label}
|
||||
badge={chipLabel(def, activeFilters[def.id])}
|
||||
/>
|
||||
<OptionChips
|
||||
options={def.options ?? []}
|
||||
selected={activeFilters[def.id]}
|
||||
multi={def.type === 'multi-select'}
|
||||
onToggle={val => {
|
||||
if (def.type === 'multi-select') {
|
||||
const curr = activeFilters[def.id] || []
|
||||
const next = curr.includes(val)
|
||||
? curr.filter(v => v !== val)
|
||||
: [...curr, val]
|
||||
onSetFilter(def.id, next.length > 0 ? next : null)
|
||||
} else {
|
||||
onSetFilter(def.id, activeFilters[def.id] === val ? null : val)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* ── Saved / custom filters ──────────────────────────────────────── */}
|
||||
{(pinnedFilters.length > 0 || savedFilters.length > 0) && (
|
||||
<>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography level='title-sm' fontWeight={600}>
|
||||
Saved Filters
|
||||
</Typography>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
onClick={() => {
|
||||
setFilterSheetOpen(false)
|
||||
// navigate to /filters or open settings
|
||||
}}
|
||||
>
|
||||
<Settings sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{savedFilters.map(filter => {
|
||||
const isActive = activeFilterId === filter.id
|
||||
return (
|
||||
<Chip
|
||||
key={filter.id}
|
||||
variant={isActive ? 'solid' : 'soft'}
|
||||
color='neutral'
|
||||
startDecorator={
|
||||
isActive ? (
|
||||
<Check sx={{ fontSize: 14 }} />
|
||||
) : (
|
||||
<Chip size='sm' variant='plain' color='neutral'>
|
||||
{filter.count ?? 0}
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
onSavedFilterClick?.(filter.id)
|
||||
setFilterSheetOpen(false)
|
||||
}}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': { opacity: 0.85 },
|
||||
...(filter.color && !isActive
|
||||
? { borderColor: filter.color }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{filter.name}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant='plain'
|
||||
color='primary'
|
||||
size='sm'
|
||||
startDecorator={<Add sx={{ fontSize: 16 }} />}
|
||||
onClick={() => {
|
||||
setFilterSheetOpen(false)
|
||||
onCreateAdvancedFilter?.()
|
||||
}}
|
||||
sx={{ mt: 1.5, alignSelf: 'flex-start', px: 0 }}
|
||||
>
|
||||
Create advanced filter
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edge case: no saved filters yet → still show "Create" CTA */}
|
||||
{savedFilters.length === 0 && (
|
||||
<>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<Button
|
||||
variant='plain'
|
||||
color='primary'
|
||||
size='sm'
|
||||
startDecorator={<Add sx={{ fontSize: 16 }} />}
|
||||
onClick={() => {
|
||||
setFilterSheetOpen(false)
|
||||
onCreateAdvancedFilter?.()
|
||||
}}
|
||||
sx={{ alignSelf: 'flex-start', px: 0 }}
|
||||
>
|
||||
Create advanced filter
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</BottomSheetModal>
|
||||
|
||||
{/* ── Display bottom sheet (View + Group combined) ───────────────────── */}
|
||||
<BottomSheetModal
|
||||
open={displaySheetOpen}
|
||||
onClose={() => setDisplaySheetOpen(false)}
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ViewAgenda sx={{ fontSize: 20 }} />
|
||||
Display
|
||||
</Box>
|
||||
}
|
||||
footer={
|
||||
<Button onClick={() => setDisplaySheetOpen(false)} sx={{ minWidth: 140 }}>
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{/* View section */}
|
||||
<SectionHeader label='View' />
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{viewOptions.map(opt => (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
variant={viewMode === opt.value ? 'solid' : 'soft'}
|
||||
color={viewMode === opt.value ? 'primary' : 'neutral'}
|
||||
startDecorator={viewMode === opt.value ? <Check sx={{ fontSize: 14 }} /> : opt.icon}
|
||||
onClick={() => onToggleViewMode?.(opt.value)}
|
||||
sx={{
|
||||
py: 0.64,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': { opacity: 0.85 },
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
|
||||
{/* Group by section */}
|
||||
<SectionHeader
|
||||
icon={<Sort />}
|
||||
label='Group by'
|
||||
badge={
|
||||
selectedGroupBy !== 'default'
|
||||
? groupByOptions.find(o => o.value === selectedGroupBy)?.label
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{groupByOptions.map(opt => (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
variant={selectedGroupBy === opt.value ? 'solid' : 'soft'}
|
||||
color={selectedGroupBy === opt.value ? 'primary' : 'neutral'}
|
||||
onClick={() => onGroupBySelect?.(opt.value)}
|
||||
sx={{
|
||||
py: 0.64,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': { opacity: 0.85 },
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Show tasks for section */}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<SectionHeader
|
||||
icon={<FilterList />}
|
||||
label='Show tasks for'
|
||||
badge={
|
||||
selectedAssigneeFilter !== 'anyone'
|
||||
? assigneeOptions.find(o => o.value === selectedAssigneeFilter)?.label
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<OptionChips
|
||||
options={assigneeOptions}
|
||||
selected={selectedAssigneeFilter}
|
||||
multi={false}
|
||||
onToggle={v => onAssigneeFilterChange?.(v)}
|
||||
/>
|
||||
|
||||
{/* Project section */}
|
||||
{showProjectSection && (
|
||||
<>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
<SectionHeader
|
||||
icon={<FolderOpen />}
|
||||
label='Project'
|
||||
badge={projectActive ? selectedProject.name : null}
|
||||
/>
|
||||
<OptionChips
|
||||
options={projects.map(p => ({ value: p.id, label: p.name }))}
|
||||
selected={selectedProject?.id ?? 'default'}
|
||||
multi={false}
|
||||
onToggle={id => {
|
||||
const p = projects.find(x => x.id === id)
|
||||
if (p) onProjectSelect?.(p)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChoreToolbar
|
||||
@@ -8,11 +8,21 @@ import {
|
||||
import '@meauxt/react-swipeable-list/dist/styles.css'
|
||||
import {
|
||||
Analytics,
|
||||
CalendarMonth,
|
||||
Check,
|
||||
Checklist,
|
||||
EventBusy,
|
||||
EventNote,
|
||||
FilterList,
|
||||
Group,
|
||||
History,
|
||||
HourglassEmpty,
|
||||
Person,
|
||||
Redo,
|
||||
RunningWithErrors,
|
||||
Schedule,
|
||||
Star,
|
||||
ThumbDown,
|
||||
Timelapse,
|
||||
TrendingUp,
|
||||
} from '@mui/icons-material'
|
||||
@@ -20,10 +30,12 @@ import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import FilterBar from '../../components/common/FilterBar'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||
import { useFilter } from '../../hooks/useFilter'
|
||||
import {
|
||||
useChoreHistory,
|
||||
useDeleteChoreHistory,
|
||||
@@ -34,6 +46,7 @@ import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ChoreHistoryStatus } from '../../utils/Chores'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import EditHistoryModal from '../Modals/EditHistoryModal'
|
||||
import HistoryDetailModal from '../Modals/HistoryDetailModal'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
||||
import HistoryCard from './HistoryCard'
|
||||
@@ -48,6 +61,7 @@ const ChoreHistory = () => {
|
||||
const { fmt } = useLocalization()
|
||||
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
|
||||
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||
const [detailModalConfig, setDetailModalConfig] = useState({ isOpen: false })
|
||||
const { showSuccess, showError } = useNotification()
|
||||
// React Query hooks
|
||||
const { data: choreHistoryData, isLoading } = useChoreHistory(choreId)
|
||||
@@ -58,6 +72,61 @@ const ChoreHistory = () => {
|
||||
const choreHistory = choreHistoryData?.res || []
|
||||
const performers = circleMembersData?.res || []
|
||||
|
||||
const filterDefs = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'status',
|
||||
label: 'Status',
|
||||
type: 'multi-select',
|
||||
icon: <FilterList />,
|
||||
options: [
|
||||
{ value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: <Check sx={{ fontSize: 14 }} /> },
|
||||
{ value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> },
|
||||
{ value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> },
|
||||
{ value: ChoreHistoryStatus.REJECTED, label: 'Rejected', color: 'danger', icon: <ThumbDown sx={{ fontSize: 14 }} /> },
|
||||
{ value: 5, label: 'Missed', color: 'danger', icon: <RunningWithErrors sx={{ fontSize: 14 }} /> },
|
||||
{ value: 6, label: 'Rescheduled', color: 'warning', icon: <Schedule sx={{ fontSize: 14 }} /> },
|
||||
],
|
||||
filterFn: (item, values) => values.includes(item.status),
|
||||
},
|
||||
{
|
||||
id: 'hasNotes',
|
||||
label: 'Has Notes',
|
||||
type: 'boolean',
|
||||
icon: <EventNote />,
|
||||
filterFn: item => !!item.notes,
|
||||
},
|
||||
{
|
||||
id: 'completedBy',
|
||||
label: 'Completed By',
|
||||
type: 'multi-select',
|
||||
icon: <Person />,
|
||||
options: performers.map(p => ({
|
||||
value: p.userId,
|
||||
label: p.displayName,
|
||||
avatar: p.image,
|
||||
})),
|
||||
filterFn: (item, values) => values.includes(item.completedBy),
|
||||
},
|
||||
{
|
||||
id: 'dateRange',
|
||||
label: 'Completed At',
|
||||
type: 'date-range',
|
||||
icon: <CalendarMonth />,
|
||||
filterFn: (item, value) => {
|
||||
const performed = new Date(item.performedAt || item.updatedAt)
|
||||
if (value.from && performed < new Date(value.from)) return false
|
||||
if (value.to && performed > new Date(value.to)) return false
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
[performers],
|
||||
)
|
||||
|
||||
const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } =
|
||||
useFilter(choreHistory, filterDefs)
|
||||
|
||||
const handleDelete = historyEntry => {
|
||||
showConfirmation(
|
||||
`Are you sure you want to delete this history record?`,
|
||||
@@ -205,9 +274,10 @@ const ChoreHistory = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
<Container maxWidth='md' sx={{ px: 0 }}>
|
||||
{/* Enhanced Header Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ gap: 2, p: 2 }}>
|
||||
{/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}> */}
|
||||
{/* Statistics Cards Grid - Compact Design */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
<History sx={{ fontSize: '1.5rem' }} />
|
||||
@@ -285,7 +355,9 @@ const ChoreHistory = () => {
|
||||
</Box>
|
||||
|
||||
{/* History Section Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
|
||||
|
||||
<Analytics sx={{ fontSize: '1.5rem' }} />
|
||||
<Typography
|
||||
level='title-md'
|
||||
@@ -294,14 +366,50 @@ const ChoreHistory = () => {
|
||||
Task Activity
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<FilterBar
|
||||
filterDefs={filterDefs}
|
||||
activeFilters={activeFilters}
|
||||
onSetFilter={setFilter}
|
||||
onClearAll={clearAll}
|
||||
resultCount={filteredHistory.length}
|
||||
totalCount={choreHistory.length}
|
||||
/>
|
||||
</Box>
|
||||
{filteredHistory.length === 0 && activeFilterCount > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
py: 6,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<FilterList sx={{ fontSize: '3rem', color: 'text.tertiary' }} />
|
||||
<Typography level='title-md' sx={{ color: 'text.secondary' }}>
|
||||
No results match your filters
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.tertiary' }}>
|
||||
Try adjusting or clearing the active filters.
|
||||
</Typography>
|
||||
<Button variant='soft' size='sm' onClick={clearAll} sx={{ mt: 0.5 }}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{filteredHistory.length > 0 && (
|
||||
<Sheet
|
||||
variant='plain'
|
||||
sx={{ borderRadius: 'sm', boxShadow: 'md', overflow: 'hidden' }}
|
||||
sx={{ borderRadius: 'sm', overflow: 'hidden' }}
|
||||
>
|
||||
{/* Chore History List (Updated Style) */}
|
||||
|
||||
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
||||
{choreHistory.map((historyEntry, index) => (
|
||||
{filteredHistory.map((historyEntry, index) => (
|
||||
<SwipeableListItem
|
||||
key={historyEntry.id || index}
|
||||
swipeActionOpen={
|
||||
@@ -366,6 +474,14 @@ const ChoreHistory = () => {
|
||||
performers={performers}
|
||||
allHistory={choreHistory}
|
||||
index={index}
|
||||
onViewDetails={() => {
|
||||
setDetailModalConfig({
|
||||
isOpen: true,
|
||||
entry: historyEntry,
|
||||
performers,
|
||||
onClose: () => setDetailModalConfig({ isOpen: false }),
|
||||
})
|
||||
}}
|
||||
onViewNote={notes => {
|
||||
setNoteViewerConfig({
|
||||
isOpen: true,
|
||||
@@ -387,6 +503,7 @@ const ChoreHistory = () => {
|
||||
))}
|
||||
</SwipeableList>
|
||||
</Sheet>
|
||||
)}
|
||||
<EditHistoryModal
|
||||
config={{
|
||||
isOpen: isEditModalOpen,
|
||||
@@ -445,7 +562,8 @@ const ChoreHistory = () => {
|
||||
/>
|
||||
<ConfirmationModal config={confirmModalConfig} />
|
||||
<NoteViewerModal config={noteViewerConfig} />
|
||||
</Container>
|
||||
<HistoryDetailModal config={detailModalConfig} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user