diff --git a/src/components/common/FilterBar.jsx b/src/components/common/FilterBar.jsx
index 65f49d8..6f221e1 100644
--- a/src/components/common/FilterBar.jsx
+++ b/src/components/common/FilterBar.jsx
@@ -1,4 +1,4 @@
-import { Check, Close, FilterList, Tune } from '@mui/icons-material'
+import { Check, FilterList, Tune } from '@mui/icons-material'
import {
Avatar,
Badge,
@@ -11,6 +11,7 @@ import {
} from '@mui/joy'
import { useState } from 'react'
import BottomSheetModal from './BottomSheetModal'
+import ActiveFilterChips from './filter/ActiveFilterChips'
/**
* Reusable filter bar component.
@@ -134,6 +135,47 @@ const FilterBar = ({
const hasActive = activeFilterCount > 0
+ const selectableChipSx = {
+ cursor: 'pointer',
+ transition: 'all 0.15s ease',
+ userSelect: 'none',
+ alignItems: 'center',
+ '& .MuiChip-startDecorator': {
+ display: 'flex',
+ alignItems: 'center',
+ mr: 0.5,
+ },
+ '& .MuiChip-label': {
+ lineHeight: 1.2,
+ },
+ '&:hover': { opacity: 0.85 },
+ }
+
+ const sectionBadgeChipSx = {
+ ml: 'auto',
+ fontSize: '0.7rem',
+ minHeight: 22,
+ py: 0.25,
+ px: 0.75,
+ alignItems: 'center',
+ '& .MuiChip-label': {
+ lineHeight: 1.2,
+ px: 0,
+ },
+ }
+
+ const modalCountChipSx = {
+ ml: 0.5,
+ minHeight: 22,
+ py: 0.25,
+ px: 0.75,
+ alignItems: 'center',
+ '& .MuiChip-label': {
+ lineHeight: 1.2,
+ px: 0,
+ },
+ }
+
// ── Chip labels for inline bar ─────────────────────────────────────────────
const getActiveChipLabel = def => {
@@ -223,85 +265,47 @@ const FilterBar = ({
color='primary'
size='sm'
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
+ sx={{ display: 'flex', alignItems: 'center' }}
>
}
onClick={() => setIsOpen(true)}
- sx={{ borderRadius: 'xl', gap: 0.5 }}
+ sx={{
+ borderRadius: 'xl',
+ py: 0.5,
+ px: 1,
+ gap: 0.5,
+ alignItems: 'center',
+ '& .MuiButton-startDecorator': {
+ display: 'flex',
+ alignItems: 'center',
+ mr: 0.5,
+ },
+ }}
>
Filters
- {(() => {
- const activeChips = filterDefs
+ ({ def, label: getActiveChipLabel(def) }))
.filter(({ label }) => !!label)
- const MAX_VISIBLE = 2
- const visible = activeChips.slice(0, MAX_VISIBLE)
- const overflow = activeChips.length - MAX_VISIBLE
-
- return (
- <>
- {visible.map(({ def, label }) => (
- {
- e.stopPropagation()
- onSetFilter(def.id, null)
- }}
- />
- }
- onClick={() => setIsOpen(true)}
- sx={{
- py: 0.64,
- cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { opacity: 0.85 } }}
- >
- {label}
-
- ))}
-
- {overflow > 0 && (
- setIsOpen(true)}
- sx={{ py: 0.64, cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { opacity: 0.85 } }}
- >
- +{overflow} more
-
- )}
- >
- )
- })()}
-
- {hasActive && (
-
- )}
-
- {hasActive && resultCount !== undefined && totalCount !== undefined && (
-
- {resultCount} / {totalCount}
-
- )}
+ .map(({ def, label }) => ({
+ key: def.id,
+ label,
+ onClear: () => onSetFilter(def.id, null),
+ }))}
+ onOpen={() => setIsOpen(true)}
+ onClearAll={hasActive ? onClearAll : undefined}
+ resultCount={hasActive ? resultCount : undefined}
+ totalCount={hasActive ? totalCount : undefined}
+ maxVisible={2}
+ chipSize='md'
+ />
{/* ── Bottom sheet ────────────────────────────────────── */}
@@ -313,7 +317,7 @@ const FilterBar = ({
Filters
{hasActive && (
-
+
{activeFilterCount}
)}
@@ -356,20 +360,20 @@ const FilterBar = ({
{/* active badge in header */}
{def.type === 'multi-select' && (activeFilters[def.id]?.length ?? 0) > 0 && (
-
+
{activeFilters[def.id].length} selected
)}
{def.type === 'single-select' && activeFilters[def.id] != null && (() => {
const opt = def.options?.find(o => o.value === activeFilters[def.id])
return opt ? (
-
+
{opt.label}
) : null
})()}
{def.type === 'date-range' && getActiveChipLabel(def) && (
-
+
{getActiveChipLabel(def)}
)}
@@ -393,7 +397,7 @@ const FilterBar = ({
) : (opt.icon ?? null)
}
onClick={() => handleMultiToggle(def.id, opt.value)}
- sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
+ sx={selectableChipSx}
>
{opt.label}
@@ -420,7 +424,7 @@ const FilterBar = ({
) : (opt.icon ?? null)
}
onClick={() => handleSingleToggle(def.id, opt.value)}
- sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
+ sx={selectableChipSx}
>
{opt.label}
@@ -436,7 +440,7 @@ const FilterBar = ({
color={activeFilters[def.id] ? 'primary' : 'neutral'}
startDecorator={activeFilters[def.id] ? : null}
onClick={() => handleBoolToggle(def.id)}
- sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
+ sx={selectableChipSx}
>
{def.label}
@@ -458,7 +462,7 @@ const FilterBar = ({
color={isSelected ? 'primary' : 'neutral'}
startDecorator={isSelected ? : null}
onClick={() => handleDateRangePreset(def.id, preset.value)}
- sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
+ sx={selectableChipSx}
>
{preset.label}
diff --git a/src/components/common/filter/ActiveFilterChips.jsx b/src/components/common/filter/ActiveFilterChips.jsx
new file mode 100644
index 0000000..4e06220
--- /dev/null
+++ b/src/components/common/filter/ActiveFilterChips.jsx
@@ -0,0 +1,131 @@
+import { Close } from '@mui/icons-material'
+import { Box, Button, Chip, Typography } from '@mui/joy'
+
+const ActiveFilterChips = ({
+ chips = [],
+ onOpen,
+ onClearAll,
+ resultCount,
+ totalCount,
+ maxVisible = 2,
+ chipSize = 'md',
+ clearButtonSize = 'sm',
+ clearButtonSx,
+ containerSx,
+ chipSx,
+ overflowChipSx,
+ resultSx,
+}) => {
+ if (!chips.length) {
+ return null
+ }
+
+ const visible = chips.slice(0, maxVisible)
+ const overflow = chips.length - maxVisible
+
+ return (
+
+ {visible.map(({ key, label, onClear, color = 'primary' }) => (
+ {
+ e.stopPropagation()
+ onClear?.()
+ }}
+ />
+ }
+ onClick={onOpen}
+ sx={{
+ cursor: 'pointer',
+ flexShrink: 0,
+ transition: 'all 0.15s ease',
+ alignItems: 'center',
+ '& .MuiChip-endDecorator': {
+ display: 'flex',
+ alignItems: 'center',
+ ml: 0.5,
+ },
+ '& .MuiChip-label': {
+ lineHeight: 1.2,
+ },
+ '&:hover': { opacity: 0.85 },
+ ...chipSx,
+ }}
+ >
+ {label}
+
+ ))}
+
+ {overflow > 0 && (
+
+ +{overflow} more
+
+ )}
+
+ {resultCount != null && totalCount != null && (
+
+ {resultCount} / {totalCount}
+
+ )}
+
+ {onClearAll && (
+
+ )}
+
+ )
+}
+
+export default ActiveFilterChips
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 3ec6ae8..a0f63f1 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -951,9 +951,11 @@ const MyChores = () => {
labels={userLabels || []}
projects={projectsWithDefault}
tempFilter={tempFilter}
+ tempFilterMeta={tempFilterMeta}
applyTempFilter={applyTempFilter}
clearTempFilter={clearTempFilter}
saveFilter={saveFilter}
+ updateFilter={updateFilter}
onFilterSaved={name =>
showSuccess({
title: 'Filter Saved',
diff --git a/src/views/Chores/components/ChoreToolbarPrototype.jsx b/src/views/Chores/components/ChoreToolbarPrototype.jsx
index 42a7fe4..9b2c3d1 100644
--- a/src/views/Chores/components/ChoreToolbarPrototype.jsx
+++ b/src/views/Chores/components/ChoreToolbarPrototype.jsx
@@ -23,9 +23,7 @@ import {
Check,
CheckBox,
CheckBoxOutlineBlank,
- Close,
FilterList,
- FolderOpen,
Save,
Sort,
Tune,
@@ -46,17 +44,23 @@ import {
MenuItem,
Typography,
} from '@mui/joy'
-import { useRef, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import BottomSheetModal from '../../../components/common/BottomSheetModal'
+import ActiveFilterChips from '../../../components/common/filter/ActiveFilterChips'
import { Z_INDEX } from '../../../constants/zIndex'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
import { FILTER_COLORS } from '../../../utils/Colors'
+import Priorities from '../../../utils/Priorities'
import FilterBuilderContent, {
+ CHORE_STATUSES,
+ DUE_DATE_OPTIONS,
+ POINTS_OPERATORS,
conditionsToSelections,
defaultSelections,
selectionsToConditions,
} from './FilterBuilderContent'
import SearchBar from './SearchBar'
+import ProjectSelector from '../../components/ProjectSelector'
// ─── sub-components for the Display sheet ────────────────────────────────────
@@ -133,9 +137,11 @@ const OptionChips = ({ options, selected, multi, onToggle }) => (
* labels – user labels for Labels section
* projects – projects list (projectsWithDefault) for Projects section + Display sheet
* tempFilter – current temp filter object { conditions, operator } or null
+ * tempFilterMeta – metadata for temp filter, including saved-filter edit source when applicable
* applyTempFilter – (filter) => void — called immediately as selections change
* clearTempFilter – () => void
* saveFilter – (filterData) => Promise — saves as a named filter
+ * updateFilter – (filterId, filterData) => Promise — updates an existing saved filter
* onFilterSaved – (name) => void — called after successful save (for notifications)
*
* -- Result counts --
@@ -176,9 +182,11 @@ const ChoreToolbar = ({
labels = [],
projects = [],
tempFilter,
+ tempFilterMeta,
applyTempFilter,
clearTempFilter,
saveFilter,
+ updateFilter,
onFilterSaved,
// result counts
resultCount,
@@ -219,7 +227,9 @@ const ChoreToolbar = ({
const [savingFilter, setSavingFilter] = useState(false)
const [saveFilterName, setSaveFilterName] = useState('')
const [saveMenuAnchorEl, setSaveMenuAnchorEl] = useState(null)
+ const [editingSavedFilter, setEditingSavedFilter] = useState(null)
const saveMenuRef = useRef(null)
+ const activeConditions = selectionsToConditions(localSelections)
// ── badge counts ─────────────────────────────────────────────────────────────
@@ -232,46 +242,166 @@ const ChoreToolbar = ({
const inlineChips = []
- if (savedFilterActive) {
- const sf = savedFilters.find(f => f.id === activeFilterId)
- if (sf) {
- inlineChips.push({
- key: '__saved',
- label: sf.name,
- onClear: () => onSavedFilterClick?.(activeFilterId),
- })
+ const getConditionChipLabel = condition => {
+ if (!condition?.type) return 'Filter'
+
+ const typeLabels = {
+ assignee: 'Assignee',
+ createdBy: 'Created By',
+ status: 'Status',
+ priority: 'Priority',
+ label: 'Labels',
+ project: 'Project',
+ dueDate: 'Due Date',
+ points: 'Points',
}
- } else if (tempConditionCount > 0) {
+
+ const typeLabel = typeLabels[condition.type] || 'Filter'
+ const prefix = condition.operator === 'isNot' ? 'Not ' : ''
+
+ if (condition.type === 'dueDate') {
+ const dueDateLabel =
+ DUE_DATE_OPTIONS.find(o => o.value === condition.operator)?.label ||
+ 'Custom'
+ return `${typeLabel}: ${dueDateLabel}`
+ }
+
+ if (condition.type === 'points') {
+ const pointsOp =
+ POINTS_OPERATORS.find(o => o.value === condition.operator)?.label ||
+ condition.operator ||
+ ''
+ return `${typeLabel} ${pointsOp} ${condition.value ?? 0}`
+ }
+
+ const rawValues = Array.isArray(condition.value)
+ ? condition.value
+ : condition.value != null
+ ? [condition.value]
+ : []
+
+ const resolveLabel = value => {
+ if (condition.type === 'assignee' || condition.type === 'createdBy') {
+ const member = members.find(m => m.userId === value)
+ return member?.displayName || member?.username || String(value)
+ }
+ if (condition.type === 'status') {
+ return CHORE_STATUSES.find(s => s.value === value)?.label || String(value)
+ }
+ if (condition.type === 'priority') {
+ return Priorities.find(p => p.value === value)?.name || String(value)
+ }
+ if (condition.type === 'label') {
+ return labels.find(l => l.id === value)?.name || String(value)
+ }
+ if (condition.type === 'project') {
+ if (value === 'default') return 'Default Project'
+ return projects.find(p => p.id === value)?.name || String(value)
+ }
+ return String(value)
+ }
+
+ if (rawValues.length === 0) {
+ return `${prefix}${typeLabel}`
+ }
+
+ if (rawValues.length === 1) {
+ return `${prefix}${typeLabel}: ${resolveLabel(rawValues[0])}`
+ }
+
+ return `${prefix}${typeLabel} (${rawValues.length})`
+ }
+
+ const clearConditionAtIndex = index => {
+ const nextConditions = (tempFilter?.conditions || []).filter(
+ (_condition, conditionIndex) => conditionIndex !== index,
+ )
+
+ if (nextConditions.length === 0) {
+ setLocalSelections(defaultSelections())
+ clearTempFilter?.()
+ return
+ }
+
+ const nextFilter = {
+ ...tempFilter,
+ operator: tempFilter?.operator || 'AND',
+ conditions: nextConditions,
+ }
+
+ setLocalSelections(conditionsToSelections(nextConditions))
+ applyTempFilter?.(nextFilter)
+ }
+
+ const activeSavedFilter = savedFilterActive
+ ? savedFilters.find(f => f.id === activeFilterId)
+ : null
+
+ const activeChipConditions = savedFilterActive
+ ? activeSavedFilter?.conditions || []
+ : tempFilter?.conditions || []
+
+ activeChipConditions.forEach((condition, index) => {
inlineChips.push({
- key: '__temp',
- label: `${tempConditionCount} condition${tempConditionCount !== 1 ? 's' : ''}`,
+ key: `${savedFilterActive ? '__saved' : '__temp'}_${index}`,
+ label: getConditionChipLabel(condition),
onClear: () => {
- setLocalSelections(defaultSelections())
- clearTempFilter?.()
+ if (savedFilterActive) {
+ onSavedFilterClick?.(activeFilterId)
+ return
+ }
+ clearConditionAtIndex(index)
},
})
- }
+ })
// ── open filter sheet ────────────────────────────────────────────────────────
const openFilterSheet = () => {
if (tempFilter?.conditions?.length > 0) {
setLocalSelections(conditionsToSelections(tempFilter.conditions))
+ if (tempFilterMeta?.sourceFilterId) {
+ const sourceFilter =
+ savedFilters.find(f => f.id === tempFilterMeta.sourceFilterId) ||
+ null
+ setEditingSavedFilter(
+ sourceFilter ||
+ (tempFilterMeta.sourceFilterId
+ ? {
+ id: tempFilterMeta.sourceFilterId,
+ name: tempFilterMeta.sourceFilterName,
+ description: tempFilterMeta.sourceFilterDescription,
+ color: tempFilterMeta.sourceFilterColor,
+ }
+ : null),
+ )
+ } else {
+ setEditingSavedFilter(null)
+ }
} else if (activeFilterId) {
const sf = savedFilters.find(f => f.id === activeFilterId)
+ setEditingSavedFilter(sf || null)
setLocalSelections(
sf?.conditions
? conditionsToSelections(sf.conditions)
: defaultSelections(),
)
} else {
+ setEditingSavedFilter(null)
setLocalSelections(defaultSelections())
}
setSavingFilter(false)
setSaveFilterName('')
+ setSaveMenuAnchorEl(null)
setFilterSheetOpen(true)
}
+ useEffect(() => {
+ if (!filterSheetOpen || savingFilter || activeConditions.length === 0) {
+ setSaveMenuAnchorEl(null)
+ }
+ }, [filterSheetOpen, savingFilter, activeConditions.length])
+
// ── selection changes → apply temp filter immediately ────────────────────────
const handleSelectionsChange = updater => {
@@ -279,7 +409,20 @@ const ChoreToolbar = ({
const next = typeof updater === 'function' ? updater(prev) : updater
const conditions = selectionsToConditions(next)
if (conditions.length > 0) {
- applyTempFilter?.({ conditions, operator: 'AND' })
+ applyTempFilter?.(
+ { conditions, operator: 'AND' },
+ editingSavedFilter
+ ? {
+ name: editingSavedFilter.name,
+ description: editingSavedFilter.description,
+ sourceFilterId: editingSavedFilter.id,
+ sourceFilterName: editingSavedFilter.name,
+ sourceFilterDescription: editingSavedFilter.description,
+ sourceFilterColor: editingSavedFilter.color,
+ isEditingSavedFilter: true,
+ }
+ : null,
+ )
} else {
clearTempFilter?.()
}
@@ -310,6 +453,31 @@ const ChoreToolbar = ({
setFilterSheetOpen(false)
}
+ const handleUpdateFilter = () => {
+ if (!editingSavedFilter?.id || !updateFilter) return
+
+ const conditions = selectionsToConditions(localSelections)
+ if (conditions.length === 0) return
+
+ updateFilter(
+ editingSavedFilter.id,
+ {
+ name: editingSavedFilter.name,
+ description: editingSavedFilter.description || '',
+ color: editingSavedFilter.color,
+ conditions,
+ operator: 'AND',
+ },
+ )?.then?.(() => {
+ clearTempFilter?.()
+ onSavedFilterClick?.(editingSavedFilter.id)
+ onFilterSaved?.(editingSavedFilter.name)
+ })
+
+ setSaveMenuAnchorEl(null)
+ setFilterSheetOpen(false)
+ }
+
// ── display sheet helpers ────────────────────────────────────────────────────
const filterActive = activeFilterId != null || tempConditionCount > 0
@@ -341,13 +509,6 @@ const ChoreToolbar = ({
{ value: 'calendar', label: 'Calendar', icon: },
]
- // Only show project in Display sheet when no advanced filter is active
- const showProjectInDisplay =
- !filterActive &&
- projects.filter(p => p.id !== 'default').length > 0
-
- const activeConditions = selectionsToConditions(localSelections)
-
return (
<>
{/* ── Row 1: main toolbar ─────────────────────────────────────────────── */}
@@ -387,6 +548,15 @@ const ChoreToolbar = ({
+ {/* Project selector */}
+ {!filterActive && projects.filter(p => p.id !== 'default').length > 0 && (
+
+ )}
+
{/* Display button — View + Group combined */}
{
+ setLocalSelections(defaultSelections())
+ onClearAllFilters?.()
}}
- >
- {inlineChips.map(({ key, label, onClear }) => (
- {
- e.stopPropagation()
- onClear()
- }}
- />
- }
- onClick={openFilterSheet}
- sx={{ cursor: 'pointer', flexShrink: 0 }}
- >
- {label}
-
- ))}
-
- {resultCount != null && totalCount != null && (
-
- {resultCount} / {totalCount}
-
- )}
-
-
-
+ resultCount={resultCount}
+ totalCount={totalCount}
+ maxVisible={2}
+ chipSize='md'
+ clearButtonSize='sm'
+ clearButtonSx={{ color: 'text.secondary' }}
+ />
)}
{/* ── Unified Filter bottom sheet ─────────────────────────────────────── */}
setFilterSheetOpen(false)}
+ onClose={() => {
+ setSaveMenuAnchorEl(null)
+ setFilterSheetOpen(false)
+ }}
maxHeight='92vh'
title={
@@ -557,6 +682,7 @@ const ChoreToolbar = ({
disabled={!hasAnyActive && activeConditions.length === 0}
onClick={() => {
setLocalSelections(defaultSelections())
+ setSaveMenuAnchorEl(null)
onClearAllFilters?.()
setFilterSheetOpen(false)
}}
@@ -568,7 +694,10 @@ const ChoreToolbar = ({
<>
>
diff --git a/src/views/Chores/hooks/useCustomFilters.js b/src/views/Chores/hooks/useCustomFilters.js
index c8c6c32..5e70af6 100644
--- a/src/views/Chores/hooks/useCustomFilters.js
+++ b/src/views/Chores/hooks/useCustomFilters.js
@@ -90,6 +90,8 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
}, [chores, activeFilter, tempFilter, context])
const applyCustomFilter = useCallback(filterId => {
+ setTempFilter(null)
+ setTempFilterMeta(null)
setActiveFilterId(filterId)
}, [])
diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx
index bfc0c35..66f5fa6 100644
--- a/src/views/User/UserActivities.jsx
+++ b/src/views/User/UserActivities.jsx
@@ -2,14 +2,18 @@ import { Cell, Pie, PieChart, Tooltip } from 'recharts'
import {
AccessTime,
+ CalendarMonth,
Check,
+ Checklist,
EventBusy,
EventNote,
Group,
HourglassEmpty,
+ Person,
Redo,
RunningWithErrors,
Schedule,
+ Style,
ThumbDown,
Timeline,
Toll,
@@ -24,23 +28,20 @@ import {
Divider,
Grid,
Link,
- Option,
- Select,
Stack,
- Tab,
- TabList,
- Tabs,
Typography,
} from '@mui/joy'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
+import FilterBar from '../../components/common/FilterBar'
+import { useFilter } from '../../hooks/useFilter'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
+import { useLabels } from '../Labels/LabelQueries'
import { ChoresGrouper } from '../../utils/Chores'
import { COLORS, TASK_COLOR } from '../../utils/Colors.jsx'
-import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import LoadingComponent from '../components/Loading'
const groupByDate = history => {
@@ -416,6 +417,7 @@ const UserActivites = () => {
choresAssigneeBreakdownChartData,
setChoresAssigneeBreakdownChartData,
] = React.useState([])
+ const { data: userLabels } = useLabels()
const { data: choresData, isLoading: isChoresLoading } = useChores(true)
const {
data: choresHistory,
@@ -432,6 +434,142 @@ const UserActivites = () => {
}
}, [circleMembersData])
+ // Client-side filters applied on top of the user+time-window slice
+ const clientFilterDefs = useMemo(
+ () => [
+ {
+ id: 'status',
+ label: 'Status',
+ type: 'multi-select',
+ icon: ,
+ options: [
+ { value: 1, label: 'Completed', color: 'success', icon: },
+ { value: 2, label: 'Skipped', color: 'warning', icon: },
+ { value: 3, label: 'Pending', color: 'neutral', icon: },
+ { value: 4, label: 'Rejected', color: 'danger', icon: },
+ { value: 5, label: 'Missed', color: 'danger', icon: },
+ { value: 6, label: 'Rescheduled', color: 'warning', icon: },
+ ],
+ filterFn: (item, values) => values.includes(item.status),
+ },
+ ...(userLabels?.length > 0
+ ? [
+ {
+ id: 'label',
+ label: 'Labels',
+ type: 'multi-select',
+ icon: ,
+ options: userLabels.map(l => ({
+ value: l.id,
+ label: l.name,
+ icon: (
+
+ ),
+ })),
+ filterFn: (item, values) =>
+ item.labelsV2?.some(l => values.includes(l.id)) ?? false,
+ },
+ ]
+ : []),
+ {
+ id: 'hasNotes',
+ label: 'Has Notes',
+ type: 'boolean',
+ icon: ,
+ filterFn: item => !!item.notes,
+ },
+ {
+ id: 'hasPoints',
+ label: 'Has Points',
+ type: 'boolean',
+ icon: ,
+ filterFn: item => (item.points ?? 0) > 0,
+ },
+ ],
+ [userLabels],
+ )
+
+ const {
+ filteredData: filteredTimeline,
+ activeFilters: clientActiveFilters,
+ setFilter: setClientFilter,
+ clearAll: clearClientFilters,
+ } = useFilter(selectedHistory, clientFilterDefs)
+
+ // All filter defs merged for FilterBar display
+ const filterDefs = useMemo(
+ () => [
+ {
+ id: 'timePeriod',
+ label: 'Time Period',
+ type: 'single-select',
+ icon: ,
+ defaultValue: 7,
+ options: [
+ { value: 7, label: '7 Days' },
+ { value: 30, label: '30 Days' },
+ { value: 90, label: '90 Days' },
+ { value: 365, label: 'All Time' },
+ ],
+ },
+ {
+ id: 'completedBy',
+ label: 'User',
+ type: 'single-select',
+ icon: ,
+ options: circleUsers.map(u => ({
+ value: u.userId,
+ label: u.displayName,
+ avatar: u.image,
+ })),
+ },
+ ...clientFilterDefs,
+ ],
+ [circleUsers, clientFilterDefs],
+ )
+
+ // Merge server-driven and client-driven active filter states for the bar
+ const activeFilters = useMemo(
+ () => ({
+ timePeriod: tabValue,
+ ...(selectedUser !== 'all' ? { completedBy: selectedUser } : {}),
+ ...clientActiveFilters,
+ }),
+ [tabValue, selectedUser, clientActiveFilters],
+ )
+
+ const handleSetFilter = (id, value) => {
+ if (id === 'completedBy') {
+ const userId = value ?? 'all'
+ setSelectedUser(userId)
+ setSelectedHistory(enrichedHistory.filter(h => USER_FILTER(h, userId)))
+ } else if (id === 'timePeriod') {
+ const days = value ?? 7
+ setTabValue(days)
+ refetchHistory(days)
+ } else {
+ setClientFilter(id, value)
+ }
+ }
+
+ const handleClearAll = () => {
+ setSelectedUser('all')
+ setSelectedHistory(enrichedHistory)
+ setTabValue(7)
+ refetchHistory(7)
+ clearClientFilters()
+ }
+
useEffect(() => {
if (
!isChoresHistoryLoading &&
@@ -444,6 +582,7 @@ const UserActivites = () => {
return {
...item,
choreName: chore?.name,
+ labelsV2: chore?.labelsV2,
}
})
setEnrichedHistory(enrichedHistory)
@@ -837,207 +976,14 @@ const UserActivites = () => {
- {/* Filter Controls - Always visible */}
-
-
-
- Filter Activities
-
-
-
- {/* User Filter */}
-
-
- Show activities for:
-
-
-
-
- {/* Time Period Filter */}
-
-
- Time period:
-
- {
- setTabValue(tabValue)
- refetchHistory(tabValue)
- }}
- value={tabValue}
- sx={{
- borderRadius: 8,
- backgroundColor: 'background.surface',
- border: '1px solid',
- borderColor: 'divider',
- }}
- >
-
- {[
- { label: '7 Days', value: 7 },
- { label: '30 Days', value: 30 },
- { label: '90 Days', value: 90 },
- { label: 'All Time', value: 365 },
- ].map((tab, index) => (
-
- {tab.label}
-
- ))}
-
-
-
-
-
-
-
- {/* Current Filter Summary */}
-
-
- Showing activities for{' '}
-
- {selectedUser === undefined || selectedUser === 'all'
- ? 'All Users'
- : circleUsers.find(user => user.userId === selectedUser)
- ?.displayName || 'Unknown User'}
- {' '}
- over the{' '}
-
- {tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
-
-
-
+
{/* Conditional Content Based on Data Availability */}
{!choresData.res?.length > 0 || !choresHistory?.length > 0 ? (
@@ -1103,7 +1049,7 @@ const UserActivites = () => {
{/* Left Side - Timeline (Mobile: Full width, Desktop: Flexible) */}
{
setNoteViewerConfig({
isOpen: true,