feat: Implement advanced filtering in FilterBar and integrate ActiveFilterChips component
- Refactored FilterBar to utilize ActiveFilterChips for displaying active filters. - Added selectable chip styles and improved hover effects for better UX. - Enhanced filter handling with new props for managing active filters and results count. - Introduced new hooks and state management for custom filters in MyChores and UserActivities. - Updated ChoreToolbar to support new filter functionalities and improved UI components. - Added client-side filtering capabilities in UserActivities with dynamic filter definitions.
This commit is contained in:
@@ -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' }}
|
||||
>
|
||||
<Button
|
||||
size='sm'
|
||||
size='md'
|
||||
variant={hasActive ? 'solid' : 'outlined'}
|
||||
color={hasActive ? 'primary' : 'neutral'}
|
||||
startDecorator={<FilterList sx={{ fontSize: 16 }} />}
|
||||
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
|
||||
</Button>
|
||||
</Badge>
|
||||
|
||||
{(() => {
|
||||
const activeChips = filterDefs
|
||||
<ActiveFilterChips
|
||||
chips={filterDefs
|
||||
.map(def => ({ 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 }) => (
|
||||
<Chip
|
||||
key={def.id}
|
||||
size='md'
|
||||
variant='soft'
|
||||
color='primary'
|
||||
endDecorator={
|
||||
<Close
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={e => {
|
||||
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}
|
||||
</Chip>
|
||||
))}
|
||||
|
||||
{overflow > 0 && (
|
||||
<Chip
|
||||
size='md'
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ py: 0.64, cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { opacity: 0.85 } }}
|
||||
>
|
||||
+{overflow} more
|
||||
</Chip>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
|
||||
{hasActive && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
sx={{ px: 0.5, fontSize: '0.75rem', color: 'text.secondary', minHeight: 0 }}
|
||||
onClick={onClearAll}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasActive && resultCount !== undefined && totalCount !== undefined && (
|
||||
<Typography level='body-xs' sx={{ color: 'text.tertiary', ml: 'auto', flexShrink: 0 }}>
|
||||
{resultCount} / {totalCount}
|
||||
</Typography>
|
||||
)}
|
||||
.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'
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* ── Bottom sheet ────────────────────────────────────── */}
|
||||
@@ -313,7 +317,7 @@ const FilterBar = ({
|
||||
<Tune sx={{ fontSize: 20 }} />
|
||||
Filters
|
||||
{hasActive && (
|
||||
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 0.5 }}>
|
||||
<Chip size='sm' variant='solid' color='primary' sx={modalCountChipSx}>
|
||||
{activeFilterCount}
|
||||
</Chip>
|
||||
)}
|
||||
@@ -356,20 +360,20 @@ const FilterBar = ({
|
||||
|
||||
{/* active badge in header */}
|
||||
{def.type === 'multi-select' && (activeFilters[def.id]?.length ?? 0) > 0 && (
|
||||
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
|
||||
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
|
||||
{activeFilters[def.id].length} selected
|
||||
</Chip>
|
||||
)}
|
||||
{def.type === 'single-select' && activeFilters[def.id] != null && (() => {
|
||||
const opt = def.options?.find(o => o.value === activeFilters[def.id])
|
||||
return opt ? (
|
||||
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
|
||||
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
) : null
|
||||
})()}
|
||||
{def.type === 'date-range' && getActiveChipLabel(def) && (
|
||||
<Chip size='sm' variant='solid' color='primary' sx={{ ml: 'auto', fontSize: '0.7rem', height: 20 }}>
|
||||
<Chip size='sm' variant='solid' color='primary' sx={sectionBadgeChipSx}>
|
||||
{getActiveChipLabel(def)}
|
||||
</Chip>
|
||||
)}
|
||||
@@ -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}
|
||||
</Chip>
|
||||
@@ -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}
|
||||
</Chip>
|
||||
@@ -436,7 +440,7 @@ const FilterBar = ({
|
||||
color={activeFilters[def.id] ? 'primary' : 'neutral'}
|
||||
startDecorator={activeFilters[def.id] ? <Check sx={{ fontSize: 14 }} /> : null}
|
||||
onClick={() => handleBoolToggle(def.id)}
|
||||
sx={{ cursor: 'pointer', transition: 'all 0.15s ease', userSelect: 'none', '&:hover': { opacity: 0.85 } }}
|
||||
sx={selectableChipSx}
|
||||
>
|
||||
{def.label}
|
||||
</Chip>
|
||||
@@ -458,7 +462,7 @@ const FilterBar = ({
|
||||
color={isSelected ? 'primary' : 'neutral'}
|
||||
startDecorator={isSelected ? <Check sx={{ fontSize: 14 }} /> : 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}
|
||||
</Chip>
|
||||
|
||||
131
src/components/common/filter/ActiveFilterChips.jsx
Normal file
131
src/components/common/filter/ActiveFilterChips.jsx
Normal file
@@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
py: 0.5,
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
...containerSx,
|
||||
}}
|
||||
>
|
||||
{visible.map(({ key, label, onClear, color = 'primary' }) => (
|
||||
<Chip
|
||||
key={key}
|
||||
size={chipSize}
|
||||
variant='soft'
|
||||
color={color}
|
||||
endDecorator={
|
||||
<Close
|
||||
sx={{ cursor: 'pointer', fontSize: chipSize === 'sm' ? 12 : 16 }}
|
||||
onClick={e => {
|
||||
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}
|
||||
</Chip>
|
||||
))}
|
||||
|
||||
{overflow > 0 && (
|
||||
<Chip
|
||||
size={chipSize}
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={onOpen}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
transition: 'all 0.15s ease',
|
||||
'&:hover': { opacity: 0.85 },
|
||||
...overflowChipSx,
|
||||
}}
|
||||
>
|
||||
+{overflow} more
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{resultCount != null && totalCount != null && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'text.tertiary',
|
||||
ml: 'auto',
|
||||
flexShrink: 0,
|
||||
...resultSx,
|
||||
}}
|
||||
>
|
||||
{resultCount} / {totalCount}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{onClearAll && (
|
||||
<Button
|
||||
size={clearButtonSize}
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
onClick={onClearAll}
|
||||
sx={{
|
||||
px: 0.5,
|
||||
fontSize: chipSize === 'sm' ? '0.72rem' : '0.75rem',
|
||||
color: 'text.secondary',
|
||||
minHeight: 0,
|
||||
flexShrink: 0,
|
||||
...clearButtonSx,
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActiveFilterChips
|
||||
@@ -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',
|
||||
|
||||
@@ -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: <CalendarMonth sx={{ fontSize: 16 }} /> },
|
||||
]
|
||||
|
||||
// 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 = ({
|
||||
</IconButton>
|
||||
</Badge>
|
||||
|
||||
{/* Project selector */}
|
||||
{!filterActive && projects.filter(p => p.id !== 'default').length > 0 && (
|
||||
<ProjectSelector
|
||||
selectedProject={selectedProject?.name || 'Default Project'}
|
||||
onProjectSelect={onProjectSelect}
|
||||
showKeyboardShortcuts={showKeyboardShortcuts}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display button — View + Group combined */}
|
||||
<IconButton
|
||||
variant={displayActive ? 'solid' : 'outlined'}
|
||||
@@ -431,74 +601,29 @@ const ChoreToolbar = ({
|
||||
|
||||
{/* ── Row 2: active filter chips ──────────────────────────────────────── */}
|
||||
{hasAnyActive && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
py: 0.5,
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
<ActiveFilterChips
|
||||
chips={inlineChips}
|
||||
onOpen={openFilterSheet}
|
||||
onClearAll={() => {
|
||||
setLocalSelections(defaultSelections())
|
||||
onClearAllFilters?.()
|
||||
}}
|
||||
>
|
||||
{inlineChips.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={openFilterSheet}
|
||||
sx={{ cursor: 'pointer', flexShrink: 0 }}
|
||||
>
|
||||
{label}
|
||||
</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={() => {
|
||||
setLocalSelections(defaultSelections())
|
||||
onClearAllFilters?.()
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</Box>
|
||||
resultCount={resultCount}
|
||||
totalCount={totalCount}
|
||||
maxVisible={2}
|
||||
chipSize='md'
|
||||
clearButtonSize='sm'
|
||||
clearButtonSx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Unified Filter bottom sheet ─────────────────────────────────────── */}
|
||||
<BottomSheetModal
|
||||
open={filterSheetOpen}
|
||||
onClose={() => setFilterSheetOpen(false)}
|
||||
onClose={() => {
|
||||
setSaveMenuAnchorEl(null)
|
||||
setFilterSheetOpen(false)
|
||||
}}
|
||||
maxHeight='92vh'
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
@@ -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 = ({
|
||||
<>
|
||||
<ButtonGroup variant='solid' color='primary'>
|
||||
<Button
|
||||
onClick={() => setFilterSheetOpen(false)}
|
||||
onClick={() => {
|
||||
setSaveMenuAnchorEl(null)
|
||||
setFilterSheetOpen(false)
|
||||
}}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
{resultCount != null
|
||||
@@ -590,14 +719,26 @@ const ChoreToolbar = ({
|
||||
placement='top-end'
|
||||
sx={{ zIndex: Z_INDEX.MODAL_CONTENT + 10 }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={handleUpdateFilter}
|
||||
disabled={!editingSavedFilter}
|
||||
>
|
||||
<Save sx={{ fontSize: 16, mr: 1 }} />
|
||||
Save Filter
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setSaveMenuAnchorEl(null)
|
||||
setSaveFilterName(
|
||||
editingSavedFilter
|
||||
? `${editingSavedFilter.name} Copy`
|
||||
: '',
|
||||
)
|
||||
setSavingFilter(true)
|
||||
}}
|
||||
>
|
||||
<Save sx={{ fontSize: 16, mr: 1 }} />
|
||||
Save as Filter
|
||||
Save as New Filter
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
@@ -605,7 +746,10 @@ const ChoreToolbar = ({
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={() => setFilterSheetOpen(false)}
|
||||
onClick={() => {
|
||||
setSaveMenuAnchorEl(null)
|
||||
setFilterSheetOpen(false)
|
||||
}}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Done
|
||||
@@ -767,26 +911,6 @@ const ChoreToolbar = ({
|
||||
onToggle={v => onAssigneeFilterChange?.(v)}
|
||||
/>
|
||||
|
||||
{/* Project section — only when no advanced filter is active */}
|
||||
{showProjectInDisplay && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
|
||||
@@ -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)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -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: <Checklist />,
|
||||
options: [
|
||||
{ value: 1, label: 'Completed', color: 'success', icon: <Check sx={{ fontSize: 14 }} /> },
|
||||
{ value: 2, label: 'Skipped', color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> },
|
||||
{ value: 3, label: 'Pending', color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> },
|
||||
{ value: 4, 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),
|
||||
},
|
||||
...(userLabels?.length > 0
|
||||
? [
|
||||
{
|
||||
id: 'label',
|
||||
label: 'Labels',
|
||||
type: 'multi-select',
|
||||
icon: <Style />,
|
||||
options: userLabels.map(l => ({
|
||||
value: l.id,
|
||||
label: l.name,
|
||||
icon: (
|
||||
<Box
|
||||
component='span'
|
||||
sx={{
|
||||
display: 'inline-block',
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
bgcolor: l.color || '#90a4ae',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
filterFn: (item, values) =>
|
||||
item.labelsV2?.some(l => values.includes(l.id)) ?? false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'hasNotes',
|
||||
label: 'Has Notes',
|
||||
type: 'boolean',
|
||||
icon: <EventNote />,
|
||||
filterFn: item => !!item.notes,
|
||||
},
|
||||
{
|
||||
id: 'hasPoints',
|
||||
label: 'Has Points',
|
||||
type: 'boolean',
|
||||
icon: <Toll />,
|
||||
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: <CalendarMonth />,
|
||||
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: <Person />,
|
||||
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 = () => {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Filter Controls - Always visible */}
|
||||
<Card
|
||||
variant='outlined'
|
||||
sx={{
|
||||
width: '100%',
|
||||
p: 2,
|
||||
mb: 3,
|
||||
borderRadius: 12,
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Typography level='title-sm' sx={{ color: 'text.secondary' }}>
|
||||
Filter Activities
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
>
|
||||
{/* User Filter */}
|
||||
<Box sx={{ flex: 1, minWidth: 200 }}>
|
||||
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
|
||||
Show activities for:
|
||||
</Typography>
|
||||
<Select
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
variant='outlined'
|
||||
value={selectedUser}
|
||||
onChange={(e, selected) => {
|
||||
setSelectedUser(selected)
|
||||
setSelectedHistory(
|
||||
enrichedHistory.filter(h => USER_FILTER(h, selected)),
|
||||
)
|
||||
}}
|
||||
renderValue={() => {
|
||||
if (selectedUser === undefined || selectedUser === 'all') {
|
||||
return (
|
||||
<Typography
|
||||
startDecorator={
|
||||
<Avatar color='primary' size='sm'>
|
||||
<Group />
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
All Users
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Typography
|
||||
startDecorator={
|
||||
<Avatar
|
||||
color='primary'
|
||||
size='sm'
|
||||
src={resolvePhotoURL(
|
||||
circleUsers.find(
|
||||
user => user.userId === selectedUser,
|
||||
)?.image,
|
||||
)}
|
||||
>
|
||||
{circleUsers
|
||||
.find(user => user.userId === selectedUser)
|
||||
?.displayName?.charAt(0)}
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
{
|
||||
circleUsers.find(user => user.userId === selectedUser)
|
||||
?.displayName
|
||||
}
|
||||
</Typography>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Option value='all'>
|
||||
<Typography
|
||||
startDecorator={
|
||||
<Avatar color='primary' size='sm'>
|
||||
<Group />
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
All Users
|
||||
</Typography>
|
||||
</Option>
|
||||
{circleUsers.map(user => (
|
||||
<Option key={user.userId} value={user.userId}>
|
||||
<Avatar
|
||||
color='primary'
|
||||
size='sm'
|
||||
src={resolvePhotoURL(user.image)}
|
||||
>
|
||||
{user.displayName?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography>{user.displayName}</Typography>
|
||||
<Chip
|
||||
color='success'
|
||||
size='sm'
|
||||
variant='soft'
|
||||
startDecorator={<Toll />}
|
||||
>
|
||||
{user.points - user.pointsRedeemed}
|
||||
</Chip>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
{/* Time Period Filter */}
|
||||
<Box sx={{ flex: 1, minWidth: 200 }}>
|
||||
<Typography level='body-sm' sx={{ mb: 1, fontWeight: 500 }}>
|
||||
Time period:
|
||||
</Typography>
|
||||
<Tabs
|
||||
onChange={(e, tabValue) => {
|
||||
setTabValue(tabValue)
|
||||
refetchHistory(tabValue)
|
||||
}}
|
||||
value={tabValue}
|
||||
sx={{
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'background.surface',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<TabList
|
||||
disableUnderline
|
||||
sx={{
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'transparent',
|
||||
p: 0.5,
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: '7 Days', value: 7 },
|
||||
{ label: '30 Days', value: 30 },
|
||||
{ label: '90 Days', value: 90 },
|
||||
{ label: 'All Time', value: 365 },
|
||||
].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
minWidth: 'auto',
|
||||
px: 2,
|
||||
py: 1,
|
||||
fontSize: 'sm',
|
||||
fontWeight: 500,
|
||||
color: 'text.secondary',
|
||||
'&.Mui-selected': {
|
||||
color: 'primary.plainColor',
|
||||
backgroundColor: 'primary.softBg',
|
||||
fontWeight: 600,
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: 'neutral.softHoverBg',
|
||||
},
|
||||
}}
|
||||
disableIndicator
|
||||
value={tab.value}
|
||||
>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Current Filter Summary */}
|
||||
<Box sx={{ mb: 3, textAlign: 'center' }}>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Showing activities for{' '}
|
||||
<Typography
|
||||
component='span'
|
||||
sx={{ fontWeight: 600, color: 'primary.500' }}
|
||||
>
|
||||
{selectedUser === undefined || selectedUser === 'all'
|
||||
? 'All Users'
|
||||
: circleUsers.find(user => user.userId === selectedUser)
|
||||
?.displayName || 'Unknown User'}
|
||||
</Typography>{' '}
|
||||
over the{' '}
|
||||
<Typography
|
||||
component='span'
|
||||
sx={{ fontWeight: 600, color: 'primary.500' }}
|
||||
>
|
||||
{tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<FilterBar
|
||||
filterDefs={filterDefs}
|
||||
activeFilters={activeFilters}
|
||||
onSetFilter={handleSetFilter}
|
||||
onClearAll={handleClearAll}
|
||||
resultCount={filteredTimeline.length}
|
||||
totalCount={selectedHistory.length}
|
||||
/>
|
||||
|
||||
{/* 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) */}
|
||||
<Box sx={{ flex: 1, minWidth: 0, width: '100%' }}>
|
||||
<ChoreHistoryTimeline
|
||||
history={selectedHistory}
|
||||
history={filteredTimeline}
|
||||
onViewNote={notes => {
|
||||
setNoteViewerConfig({
|
||||
isOpen: true,
|
||||
|
||||
Reference in New Issue
Block a user