remove the old filter logic and use advance filter in myChore
This commit is contained in:
@@ -1,16 +1,5 @@
|
||||
import { Save } from '@mui/icons-material'
|
||||
import {
|
||||
CalendarMonth,
|
||||
Check,
|
||||
FolderOpen,
|
||||
Label,
|
||||
Person,
|
||||
PriorityHigh,
|
||||
Save,
|
||||
Stars,
|
||||
TaskAlt,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
@@ -20,125 +9,15 @@ import {
|
||||
} from '@mui/joy'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import BottomSheetModal from '../../../components/common/BottomSheetModal'
|
||||
import FilterBuilderContent, {
|
||||
conditionsToSelections,
|
||||
defaultSelections,
|
||||
selectionsToConditions,
|
||||
} from '../../Chores/components/FilterBuilderContent'
|
||||
import { FILTER_COLORS } from '../../../utils/Colors'
|
||||
import { applyFilter } from '../../../utils/FilterEngine'
|
||||
import Priorities from '../../../utils/Priorities'
|
||||
import { useFilters } from '../../Filters/FilterQueries'
|
||||
|
||||
const DUE_DATE_OPTIONS = [
|
||||
{ value: 'isOverdue', label: 'Overdue', color: 'danger' },
|
||||
{ value: 'isDueToday', label: 'Today', color: 'warning' },
|
||||
{ value: 'isDueTomorrow', label: 'Tomorrow', color: 'primary' },
|
||||
{ value: 'isDueThisWeek', label: 'This Week', color: 'primary' },
|
||||
{ value: 'isDueThisMonth', label: 'This Month', color: 'neutral' },
|
||||
{ value: 'hasNoDueDate', label: 'No Due Date', color: 'neutral' },
|
||||
{ value: 'hasDueDate', label: 'Has Due Date', color: 'neutral' },
|
||||
]
|
||||
|
||||
const POINTS_OPERATORS = [
|
||||
{ value: 'greaterThan', label: '>' },
|
||||
{ value: 'greaterThanOrEqual', label: '>=' },
|
||||
{ value: 'equals', label: '=' },
|
||||
{ value: 'lessThanOrEqual', label: '<=' },
|
||||
{ value: 'lessThan', label: '<' },
|
||||
]
|
||||
|
||||
const CHORE_STATUSES = [
|
||||
{ value: 0, label: 'Active' },
|
||||
{ value: 1, label: 'Started' },
|
||||
{ value: 2, label: 'In Progress' },
|
||||
{ value: 3, label: 'Pending Approval' },
|
||||
]
|
||||
|
||||
const defaultSelections = () => ({
|
||||
assignee: { operator: 'is', values: [] },
|
||||
createdBy: { operator: 'is', values: [] },
|
||||
status: { operator: 'is', values: [] },
|
||||
priority: { operator: 'is', values: [] },
|
||||
label: { operator: 'is', values: [] },
|
||||
project: { operator: 'is', values: [] },
|
||||
dueDate: { operator: null },
|
||||
points: { operator: 'greaterThan', value: 0, active: false },
|
||||
})
|
||||
|
||||
const conditionsToSelections = conditions => {
|
||||
const sel = defaultSelections()
|
||||
if (!conditions) return sel
|
||||
conditions.forEach(c => {
|
||||
if (c.type === 'dueDate') {
|
||||
sel.dueDate = { operator: c.operator }
|
||||
} else if (c.type === 'points') {
|
||||
sel.points = { operator: c.operator, value: c.value ?? 0, active: true }
|
||||
} else if (c.type in sel) {
|
||||
sel[c.type] = {
|
||||
operator: c.operator ?? 'is',
|
||||
values: Array.isArray(c.value) ? c.value : c.value != null ? [c.value] : [],
|
||||
}
|
||||
}
|
||||
})
|
||||
return sel
|
||||
}
|
||||
|
||||
const selectionsToConditions = selections => {
|
||||
const conditions = []
|
||||
;['assignee', 'createdBy', 'status', 'priority', 'label', 'project'].forEach(type => {
|
||||
if (selections[type].values?.length > 0) {
|
||||
conditions.push({
|
||||
type,
|
||||
operator: selections[type].operator,
|
||||
value: selections[type].values,
|
||||
})
|
||||
}
|
||||
})
|
||||
if (selections.dueDate.operator) {
|
||||
conditions.push({ type: 'dueDate', operator: selections.dueDate.operator, value: null })
|
||||
}
|
||||
if (selections.points.active) {
|
||||
conditions.push({
|
||||
type: 'points',
|
||||
operator: selections.points.operator,
|
||||
value: selections.points.value,
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
const hasAnySelection = selections => selectionsToConditions(selections).length > 0
|
||||
|
||||
// ── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
const SectionHeader = ({ icon, label, children }) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Box sx={{ color: 'text.secondary', display: 'flex', alignItems: 'center', '& svg': { fontSize: 18 } }}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography level='title-sm' sx={{ fontWeight: 600 }}>{label}</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
|
||||
const IncludeExcludeToggle = ({ value, onChange, labels = ['Include', 'Exclude'] }) => (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, ml: 'auto' }}>
|
||||
{[
|
||||
{ op: 'is', label: labels[0] },
|
||||
{ op: 'isNot', label: labels[1] },
|
||||
].map(o => (
|
||||
<Chip
|
||||
key={o.op}
|
||||
size='sm'
|
||||
variant={value === o.op ? 'solid' : 'soft'}
|
||||
color={value === o.op ? (o.op === 'isNot' ? 'danger' : 'primary') : 'neutral'}
|
||||
onClick={() => onChange(o.op)}
|
||||
sx={{ cursor: 'pointer', userSelect: 'none', transition: 'all 0.15s ease' }}
|
||||
>
|
||||
{o.label}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
const AdvancedFilterBuilder = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -194,32 +73,7 @@ const AdvancedFilterBuilder = ({
|
||||
c => c.nextDueDate && new Date(c.nextDueDate) < new Date(),
|
||||
).length
|
||||
|
||||
// ── Mutators ───────────────────────────────────────────────────────────────
|
||||
|
||||
const toggleValue = (type, value) => {
|
||||
setSelections(prev => {
|
||||
const cur = prev[type].values || []
|
||||
const next = cur.includes(value) ? cur.filter(v => v !== value) : [...cur, value]
|
||||
return { ...prev, [type]: { ...prev[type], values: next } }
|
||||
})
|
||||
}
|
||||
|
||||
const setOperator = (type, op) =>
|
||||
setSelections(prev => ({ ...prev, [type]: { ...prev[type], operator: op } }))
|
||||
|
||||
const toggleDueDate = op =>
|
||||
setSelections(prev => ({
|
||||
...prev,
|
||||
dueDate: { operator: prev.dueDate.operator === op ? null : op },
|
||||
}))
|
||||
|
||||
const setPointsOperator = op =>
|
||||
setSelections(prev => ({ ...prev, points: { ...prev.points, operator: op, active: true } }))
|
||||
|
||||
const setPointsValue = val =>
|
||||
setSelections(prev => ({ ...prev, points: { ...prev.points, value: val, active: val > 0 } }))
|
||||
|
||||
// ── Save ───────────────────────────────────────────────────────────────────
|
||||
const activeConditionCount = conditions.length
|
||||
|
||||
const handleSave = () => {
|
||||
if (!filterName.trim()) {
|
||||
@@ -245,65 +99,6 @@ const AdvancedFilterBuilder = ({
|
||||
onClose()
|
||||
}
|
||||
|
||||
// ── Render helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
const chipRow = (type, options, getChipProps) => {
|
||||
const selected = selections[type].values || []
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{options.map(opt => {
|
||||
const isSelected = selected.includes(opt.value)
|
||||
const extra = getChipProps ? getChipProps(opt, isSelected) : {}
|
||||
return (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
variant={isSelected ? 'solid' : 'soft'}
|
||||
color={isSelected ? (extra.color ?? 'primary') : 'neutral'}
|
||||
startDecorator={
|
||||
isSelected ? <Check sx={{ fontSize: 14 }} /> : (extra.startDecorator ?? null)
|
||||
}
|
||||
onClick={() => toggleValue(type, opt.value)}
|
||||
sx={{ cursor: 'pointer', userSelect: 'none', transition: 'all 0.15s ease' }}
|
||||
>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const personChipRow = type => {
|
||||
const selected = selections[type].values || []
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{members.map(m => {
|
||||
const isSelected = selected.includes(m.userId)
|
||||
return (
|
||||
<Chip
|
||||
key={m.userId}
|
||||
variant={isSelected ? 'solid' : 'soft'}
|
||||
color={isSelected ? 'primary' : 'neutral'}
|
||||
startDecorator={
|
||||
isSelected ? (
|
||||
<Check sx={{ fontSize: 14 }} />
|
||||
) : (
|
||||
<Avatar src={m.image} alt={m.displayName} sx={{ '--Avatar-size': '20px' }} />
|
||||
)
|
||||
}
|
||||
onClick={() => toggleValue(type, m.userId)}
|
||||
sx={{ cursor: 'pointer', userSelect: 'none', transition: 'all 0.15s ease' }}
|
||||
>
|
||||
{m.displayName || m.username}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const activeConditionCount = conditions.length
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
open={isOpen}
|
||||
@@ -320,7 +115,14 @@ const AdvancedFilterBuilder = ({
|
||||
</Box>
|
||||
}
|
||||
footer={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{/* Preview */}
|
||||
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
|
||||
{conditions.length > 0 ? (
|
||||
@@ -360,27 +162,37 @@ const AdvancedFilterBuilder = ({
|
||||
}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
|
||||
{/* ── Name ─────────────────────────────────────────────────────────── */}
|
||||
{/* Name */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography level='body-xs' sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
Filter Name
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder='e.g. Overdue tasks for Alice'
|
||||
value={filterName}
|
||||
onChange={e => { setFilterName(e.target.value); setError('') }}
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
error={!!error}
|
||||
autoFocus
|
||||
/>
|
||||
{error && (
|
||||
<Typography level='body-xs' color='danger' sx={{ mt: 0.5 }}>{error}</Typography>
|
||||
<Typography level='body-xs' color='danger' sx={{ mt: 0.5 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ── Color ────────────────────────────────────────────────────────── */}
|
||||
{/* Color */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography level='body-xs' sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ mb: 0.75, color: 'text.secondary', fontWeight: 600 }}
|
||||
>
|
||||
Color
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
@@ -395,9 +207,10 @@ const AdvancedFilterBuilder = ({
|
||||
borderRadius: '50%',
|
||||
background: c.value,
|
||||
cursor: 'pointer',
|
||||
outline: filterColor === c.value
|
||||
? '3px solid var(--joy-palette-primary-500)'
|
||||
: '2px solid transparent',
|
||||
outline:
|
||||
filterColor === c.value
|
||||
? '3px solid var(--joy-palette-primary-500)'
|
||||
: '2px solid transparent',
|
||||
outlineOffset: '2px',
|
||||
transition: 'all 0.15s ease',
|
||||
flexShrink: 0,
|
||||
@@ -410,185 +223,13 @@ const AdvancedFilterBuilder = ({
|
||||
|
||||
<Divider sx={{ mb: 2.5 }} />
|
||||
|
||||
{/* ── Assignee ─────────────────────────────────────────────────────── */}
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
<SectionHeader icon={<Person />} label='Assignee'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.assignee.operator}
|
||||
onChange={op => setOperator('assignee', op)}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{personChipRow('assignee')}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Created By ───────────────────────────────────────────────────── */}
|
||||
{members.length > 0 && (
|
||||
<>
|
||||
<SectionHeader icon={<Person />} label='Created By'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.createdBy.operator}
|
||||
onChange={op => setOperator('createdBy', op)}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{personChipRow('createdBy')}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Status ───────────────────────────────────────────────────────── */}
|
||||
<SectionHeader icon={<TaskAlt />} label='Status'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.status.operator}
|
||||
onChange={op => setOperator('status', op)}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{chipRow('status', CHORE_STATUSES)}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
|
||||
{/* ── Priority ─────────────────────────────────────────────────────── */}
|
||||
<SectionHeader icon={<PriorityHigh />} label='Priority'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.priority.operator}
|
||||
onChange={op => setOperator('priority', op)}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{chipRow('priority', Priorities.map(p => ({ value: p.value, label: p.name })), (opt, isSelected) => ({
|
||||
color: isSelected
|
||||
? (Priorities.find(p => p.value === opt.value)?.color || 'primary')
|
||||
: 'neutral',
|
||||
startDecorator: !isSelected
|
||||
? Priorities.find(p => p.value === opt.value)?.icon
|
||||
: null,
|
||||
}))}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
|
||||
{/* ── Due Date ─────────────────────────────────────────────────────── */}
|
||||
<SectionHeader icon={<CalendarMonth />} label='Due Date' />
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{DUE_DATE_OPTIONS.map(opt => {
|
||||
const isSelected = selections.dueDate.operator === opt.value
|
||||
return (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
variant={isSelected ? 'solid' : 'soft'}
|
||||
color={isSelected ? (opt.color ?? 'primary') : 'neutral'}
|
||||
startDecorator={isSelected ? <Check sx={{ fontSize: 14 }} /> : null}
|
||||
onClick={() => toggleDueDate(opt.value)}
|
||||
sx={{ cursor: 'pointer', userSelect: 'none', transition: 'all 0.15s ease' }}
|
||||
>
|
||||
{opt.label}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
|
||||
{/* ── Labels ───────────────────────────────────────────────────────── */}
|
||||
{labels.length > 0 && (
|
||||
<>
|
||||
<SectionHeader icon={<Label />} label='Labels'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.label.operator}
|
||||
onChange={op => setOperator('label', op)}
|
||||
labels={['Has', "Doesn't Have"]}
|
||||
/>
|
||||
</SectionHeader>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{labels.map(lbl => {
|
||||
const isSelected = selections.label.values.includes(lbl.id)
|
||||
return (
|
||||
<Chip
|
||||
key={lbl.id}
|
||||
variant={isSelected ? 'solid' : 'soft'}
|
||||
color='neutral'
|
||||
startDecorator={
|
||||
<Box
|
||||
sx={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
bgcolor: lbl.color || '#90a4ae',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
endDecorator={isSelected ? <Check sx={{ fontSize: 12 }} /> : null}
|
||||
onClick={() => toggleValue('label', lbl.id)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
transition: 'all 0.15s ease',
|
||||
...(isSelected && { outline: '2px solid', outlineColor: 'primary.400' }),
|
||||
}}
|
||||
>
|
||||
{lbl.name}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Projects ─────────────────────────────────────────────────────── */}
|
||||
{projects.length > 0 && (
|
||||
<>
|
||||
<SectionHeader icon={<FolderOpen />} label='Projects'>
|
||||
<IncludeExcludeToggle
|
||||
value={selections.project.operator}
|
||||
onChange={op => setOperator('project', op)}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{chipRow(
|
||||
'project',
|
||||
[
|
||||
{ value: 'default', label: 'Default Project' },
|
||||
...projects.filter(p => p.id !== 'default').map(p => ({ value: p.id, label: p.name })),
|
||||
],
|
||||
)}
|
||||
<Divider sx={{ my: 2.5 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Points ───────────────────────────────────────────────────────── */}
|
||||
<SectionHeader icon={<Stars />} label='Points' />
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{POINTS_OPERATORS.map(op => (
|
||||
<Chip
|
||||
key={op.value}
|
||||
size='sm'
|
||||
variant={selections.points.operator === op.value && selections.points.active ? 'solid' : 'soft'}
|
||||
color={selections.points.operator === op.value && selections.points.active ? 'primary' : 'neutral'}
|
||||
onClick={() => setPointsOperator(op.value)}
|
||||
sx={{ cursor: 'pointer', userSelect: 'none', fontFamily: 'monospace', fontWeight: 600 }}
|
||||
>
|
||||
{op.label}
|
||||
</Chip>
|
||||
))}
|
||||
<Input
|
||||
type='number'
|
||||
size='sm'
|
||||
value={selections.points.value}
|
||||
onChange={e => setPointsValue(parseInt(e.target.value) || 0)}
|
||||
sx={{ width: 80 }}
|
||||
slotProps={{ input: { min: 0 } }}
|
||||
/>
|
||||
{selections.points.active && (
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
onClick={() => setSelections(prev => ({ ...prev, points: { ...prev.points, active: false, value: 0 } }))}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Clear
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<FilterBuilderContent
|
||||
selections={selections}
|
||||
onSelectionsChange={setSelections}
|
||||
members={members}
|
||||
labels={labels}
|
||||
projects={projects}
|
||||
/>
|
||||
</Box>
|
||||
</BottomSheetModal>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user