diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx
index 31a9636..2c36cb7 100644
--- a/src/contexts/RouterContext.jsx
+++ b/src/contexts/RouterContext.jsx
@@ -30,6 +30,7 @@ import PaymentCancelledView from '../views/Payments/PaymentFailView'
import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
import ProjectView from '../views/Projects/ProjectView'
+import FilterView from '../views/Filters/FilterView'
import APITokenSettings from '../views/Settings/APITokenSettings'
import MFASettings from '../views/Settings/MFASettings'
import NotificationSetting from '../views/Settings/NotificationSetting'
@@ -233,6 +234,10 @@ const Router = createBrowserRouter([
path: 'projects/',
element: ,
},
+ {
+ path: 'filters/',
+ element: ,
+ },
{
path: '*',
element: ,
diff --git a/src/utils/Colors.jsx b/src/utils/Colors.jsx
index 87780ff..89768f0 100644
--- a/src/utils/Colors.jsx
+++ b/src/utils/Colors.jsx
@@ -26,6 +26,10 @@ const LABEL_COLORS = [
{ name: 'Sand', value: '#d7ccc8' },
]
+export const FILTER_COLORS = [
+ ...LABEL_COLORS.filter(color => color.name !== 'Default'),
+]
+
export const COLORS = {
salmon: '#ff7961',
teal: '#26a69a',
diff --git a/src/utils/CustomFilterStorage.js b/src/utils/CustomFilterStorage.js
index a2a6695..640f687 100644
--- a/src/utils/CustomFilterStorage.js
+++ b/src/utils/CustomFilterStorage.js
@@ -52,6 +52,8 @@ export const saveFilter = (filter) => {
const newFilter = {
id: generateFilterId(),
name: filter.name,
+ description: filter.description || '',
+ color: filter.color || null,
icon: filter.icon || null,
conditions: filter.conditions,
operator: filter.operator || 'AND',
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 67eb512..2a69675 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -34,7 +34,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useChores } from '../../queries/ChoreQueries'
import { useNotification } from '../../service/NotificationProvider'
-import { TASK_COLOR } from '../../utils/Colors'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import { useLabels } from '../Labels/LabelQueries'
@@ -60,7 +59,6 @@ import CalendarDual from '../components/CalendarDual'
import CalendarMonthly from '../components/CalendarMonthly.jsx'
import ProjectSelector from '../components/ProjectSelector'
import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
-import SaveFilterModal from '../Modals/Inputs/SaveFilterModal'
import { useProjects } from '../Projects/ProjectQueries.js'
import ChoreModals from './components/ChoreModals'
import FilterSection from './components/FilterSection'
@@ -179,7 +177,6 @@ const MyChores = () => {
projectsWithDefault,
)
- const [showSaveFilterModal, setShowSaveFilterModal] = useState(false)
const [showAdvancedFilterBuilder, setShowAdvancedFilterBuilder] =
useState(false)
const [editingFilter, setEditingFilter] = useState(null)
@@ -736,6 +733,10 @@ const MyChores = () => {
setFilteredChores(chores)
setSearchFilter('All')
}}
+ onCreateNewFilter={() => {
+ setShowAdvancedFilterBuilder(true)
+ setEditingFilter(null)
+ }}
mouseClickHandler={handleMenuOutsideClick}
/>
@@ -1100,7 +1101,7 @@ const MyChores = () => {
{viewMode === 'calendar' && (
<>
{/* Summary Chips when no date selected */}
- {
Pending Approval
)}
-
+ */}
{/* Calendar Monthly View */}
{isLargeScreen ? (
@@ -1467,38 +1468,6 @@ const MyChores = () => {
onClose={closeModal}
/>
- {/* Save Filter Modal */}
- setShowSaveFilterModal(false)}
- onSave={filter => {
- saveFilter(filter)
- showSuccess({
- title: 'Filter Saved',
- message: `"${filter.name}" has been saved successfully`,
- })
- }}
- filterData={createFilterFromCurrentState({
- selectedProject,
- selectedChoreFilter,
- searchFilter,
- })}
- previewChores={
- activeFilterId ? customFilteredChores : searchFilteredChores
- }
- previewCount={
- activeFilterId
- ? customFilteredChores.length
- : searchFilteredChores.length
- }
- previewOverdueCount={
- (activeFilterId ? customFilteredChores : searchFilteredChores).filter(
- chore =>
- chore.nextDueDate && new Date(chore.nextDueDate) < new Date(),
- ).length
- }
- />
-
{/* Advanced Filter Builder */}
{
// Update existing filter
updateFilter(filter.id, {
name: filter.name,
+ description: filter.description,
+ color: filter.color,
conditions: filter.conditions,
operator: filter.operator,
})
diff --git a/src/views/Chores/SortAndGrouping.jsx b/src/views/Chores/SortAndGrouping.jsx
index e5d0ded..413b2ca 100644
--- a/src/views/Chores/SortAndGrouping.jsx
+++ b/src/views/Chores/SortAndGrouping.jsx
@@ -23,6 +23,7 @@ const SortAndGrouping = ({
isActive,
useChips,
title,
+ onCreateNewFilter,
}) => {
const [anchorEl, setAnchorEl] = useState(null)
const menuRef = useRef(null)
@@ -201,6 +202,22 @@ const SortAndGrouping = ({
/>
Assigned to others
+
+
+
+
+
{/*
// i need this but i think it have a bad UX and confusing so commenting it for now
)}
- {/* Create Advanced Filter Button */}
- {!activeFilterId && (
-
- }
- onClick={onCreateAdvancedFilter}
- >
- Create Advanced Filter
-
-
- )}
-
{/* Active Custom Filter Display */}
- {activeFilter && (
-
-
-
-
- }
- >
- Filter: {activeFilter.name} ({activeFilter.count} tasks
- {activeFilter.overdueCount > 0 &&
- `, ${activeFilter.overdueCount} overdue`}
- )
- {hasProjectConditions && (
-
- Cross-Project
-
- )}
-
-
- )}
>
)
}
diff --git a/src/views/Chores/hooks/useChoreFilters.js b/src/views/Chores/hooks/useChoreFilters.js
index fccb5e5..8153f1d 100644
--- a/src/views/Chores/hooks/useChoreFilters.js
+++ b/src/views/Chores/hooks/useChoreFilters.js
@@ -1,6 +1,6 @@
-import { useState, useMemo, useCallback } from 'react'
import Fuse from 'fuse.js'
-import { filterByProject, ChoreFilters } from '../../../utils/Chores'
+import { useCallback, useMemo, useState } from 'react'
+import { ChoreFilters, filterByProject } from '../../../utils/Chores'
export const useChoreFilters = ({
chores,
diff --git a/src/views/Chores/hooks/useCustomFilters.js b/src/views/Chores/hooks/useCustomFilters.js
index 1f3449b..2937013 100644
--- a/src/views/Chores/hooks/useCustomFilters.js
+++ b/src/views/Chores/hooks/useCustomFilters.js
@@ -1,20 +1,19 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useUserProfile } from '../../../queries/UserQueries'
-import {
- applyFilter,
- getFilterCount,
- getFilterOverdueCount,
- validateFilter,
-} from '../../../utils/FilterEngine'
import {
deleteFilter as deleteFilterStorage,
- getFilterById,
getSavedFilters,
saveFilter as saveFilterStorage,
toggleFilterPin,
trackFilterUsage,
updateFilter as updateFilterStorage,
} from '../../../utils/CustomFilterStorage'
+import {
+ applyFilter,
+ getFilterCount,
+ getFilterOverdueCount,
+ validateFilter,
+} from '../../../utils/FilterEngine'
export const useCustomFilters = (chores, membersData, labels, projects) => {
const { data: userProfile } = useUserProfile()
@@ -52,13 +51,15 @@ export const useCustomFilters = (chores, membersData, labels, projects) => {
? getFilterOverdueCount(chores, filter, context)
: 0
- return {
+ const result = {
...filter,
count,
overdueCount,
isValid: validation.isValid,
validationIssues: validation.issues,
}
+
+ return result
})
}, [savedFilters, chores, context])
diff --git a/src/views/Filters/FilterView.jsx b/src/views/Filters/FilterView.jsx
new file mode 100644
index 0000000..3af21b2
--- /dev/null
+++ b/src/views/Filters/FilterView.jsx
@@ -0,0 +1,823 @@
+import DeleteIcon from '@mui/icons-material/Delete'
+import EditIcon from '@mui/icons-material/Edit'
+import {
+ Avatar,
+ Box,
+ Chip,
+ CircularProgress,
+ Container,
+ IconButton,
+ Stack,
+ Typography,
+} from '@mui/joy'
+import { useEffect, useRef, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+
+import { Add, FilterAlt, Star, StarBorder, Task } from '@mui/icons-material'
+import { useChores } from '../../queries/ChoreQueries'
+import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
+import {
+ deleteFilter,
+ getSavedFilters,
+ saveFilter,
+ toggleFilterPin,
+ updateFilter,
+} from '../../utils/CustomFilterStorage'
+import { getFilterCount, getFilterOverdueCount } from '../../utils/FilterEngine'
+import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
+
+import { useLabels } from '../Labels/LabelQueries'
+import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder'
+import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
+import { useProjects } from '../Projects/ProjectQueries'
+
+const FilterCard = ({
+ filter,
+ onEditClick,
+ onDeleteClick,
+ onPinClick,
+ taskCount = 0,
+ overdueCount = 0,
+}) => {
+ const navigate = useNavigate()
+
+ // Swipe functionality state
+ const [swipeTranslateX, setSwipeTranslateX] = useState(0)
+ const [isDragging, setIsDragging] = useState(false)
+ const [isSwipeRevealed, setIsSwipeRevealed] = useState(false)
+ const [hoverTimer, setHoverTimer] = useState(null)
+ const swipeThreshold = 80
+ const maxSwipeDistance = 200 // Increased to fit pin + edit + delete
+ const dragStartX = useRef(0)
+ const cardRef = useRef(null)
+
+ // Swipe gesture handlers
+ const handleTouchStart = e => {
+ dragStartX.current = e.touches[0].clientX
+ setIsDragging(true)
+ }
+
+ const handleTouchMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.touches[0].clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleTouchEnd = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const handleMouseDown = e => {
+ dragStartX.current = e.clientX
+ setIsDragging(true)
+ }
+
+ const handleMouseMove = e => {
+ if (!isDragging) return
+
+ const currentX = e.clientX
+ const deltaX = currentX - dragStartX.current
+
+ if (isSwipeRevealed) {
+ if (deltaX > 0) {
+ const clampedDelta = Math.min(deltaX - maxSwipeDistance, 0)
+ setSwipeTranslateX(clampedDelta)
+ }
+ } else {
+ if (deltaX < 0) {
+ const clampedDelta = Math.max(deltaX, -maxSwipeDistance)
+ setSwipeTranslateX(clampedDelta)
+ }
+ }
+ }
+
+ const handleMouseUp = () => {
+ if (!isDragging) return
+ setIsDragging(false)
+
+ if (isSwipeRevealed) {
+ if (swipeTranslateX > -swipeThreshold) {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ } else {
+ setSwipeTranslateX(-maxSwipeDistance)
+ }
+ } else {
+ if (Math.abs(swipeTranslateX) > swipeThreshold) {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ } else {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+ }
+ }
+
+ const resetSwipe = () => {
+ setSwipeTranslateX(0)
+ setIsSwipeRevealed(false)
+ }
+
+ // Hover functionality for desktop
+ const handleMouseEnter = () => {
+ if (isSwipeRevealed) return
+ const timer = setTimeout(() => {
+ setSwipeTranslateX(-maxSwipeDistance)
+ setIsSwipeRevealed(true)
+ setHoverTimer(null)
+ }, 800)
+ setHoverTimer(timer)
+ }
+
+ const handleMouseLeave = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ if (!isSwipeRevealed) {
+ const hideTimer = setTimeout(() => {
+ resetSwipe()
+ }, 300)
+ setHoverTimer(hideTimer)
+ }
+ }
+
+ const handleActionAreaMouseEnter = () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ }
+
+ const handleActionAreaMouseLeave = () => {
+ if (isSwipeRevealed) {
+ resetSwipe()
+ }
+ }
+
+ // Clean up timer on unmount
+ useEffect(() => {
+ return () => {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ }
+ }
+ }, [hoverTimer])
+
+ // Get condition labels for display
+ const getConditionSummary = () => {
+ if (!filter.conditions || filter.conditions.length === 0) {
+ return 'No conditions'
+ }
+ if (filter.conditions.length === 1) {
+ return '1 condition'
+ }
+ return `${filter.conditions.length} conditions`
+ }
+
+ return (
+
+ {
+ if (hoverTimer) {
+ clearTimeout(hoverTimer)
+ setHoverTimer(null)
+ }
+ }}
+ >
+ {/* Action buttons underneath (revealed on swipe) */}
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onPinClick(filter.id)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 0.5,
+ }}
+ >
+ {filter.isPinned ? (
+
+ ) : (
+
+ )}
+
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onEditClick(filter)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 0.5,
+ }}
+ >
+
+
+
+ {
+ e.stopPropagation()
+ resetSwipe()
+ onDeleteClick(filter.id)
+ }}
+ sx={{
+ width: 40,
+ height: 40,
+ mx: 0.5,
+ }}
+ >
+
+
+
+
+ {/* Main card content */}
+ {
+ if (isSwipeRevealed) {
+ resetSwipe()
+ return
+ }
+ // Navigate to MyChores with filter applied via URL param
+ navigate(`/chores?filterId=${encodeURIComponent(filter.id)}`)
+ }}
+ onTouchStart={handleTouchStart}
+ onTouchMove={handleTouchMove}
+ onTouchEnd={handleTouchEnd}
+ onMouseDown={handleMouseDown}
+ onMouseMove={handleMouseMove}
+ onMouseUp={handleMouseUp}
+ >
+ {/* Right drag area */}
+
+ {/* Drag indicator dots */}
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ {/* Filter Icon */}
+
+
+
+
+
+
+ {/* Content - Center */}
+
+ {/* Filter Name */}
+
+
+ {filter.name}
+
+ {filter.isPinned && (
+
+ )}
+
+
+ {/* Filter Info */}
+
+ {filter.description && (
+
+ {filter.description}
+
+ )}
+
+ }
+ color={overdueCount > 0 ? 'danger' : 'primary'}
+ sx={{
+ fontSize: 10,
+ height: 18,
+ px: 0.75,
+ bgcolor:
+ overdueCount > 0 ? 'danger.softBg' : 'primary.softBg',
+ color: overdueCount > 0 ? 'danger.500' : 'primary.500',
+ }}
+ >
+ {taskCount} tasks
+
+
+ {overdueCount > 0 && (
+
+ {overdueCount} overdue
+
+ )}
+
+ }
+ sx={{
+ fontSize: 10,
+ height: 18,
+ px: 0.75,
+ bgcolor: 'neutral.softBg',
+ color: 'neutral.600',
+ }}
+ >
+ {getConditionSummary()}
+
+
+ {filter.usageCount > 0 && (
+
+ Used {filter.usageCount}x
+
+ )}
+
+
+
+
+
+ )
+}
+
+const FilterView = () => {
+ const { data: userProfile } = useUserProfile()
+ const { data: chores = { res: [] } } = useChores(false)
+ const { data: labels = [] } = useLabels()
+ const { data: projects = [] } = useProjects()
+ const { data: membersData } = useCircleMembers()
+
+ const [savedFilters, setSavedFilters] = useState([])
+ const [filterCounts, setFilterCounts] = useState({})
+ const [showAdvancedFilterBuilder, setShowAdvancedFilterBuilder] =
+ useState(false)
+ const [editingFilter, setEditingFilter] = useState(null)
+ const [confirmationModel, setConfirmationModel] = useState({})
+ const [isLoading, setIsLoading] = useState(true)
+
+ // Load filters
+ const loadFilters = () => {
+ try {
+ const filters = getSavedFilters()
+ // Sort: pinned first, then by usage count, then by last used
+ const sortedFilters = filters.sort((a, b) => {
+ if (a.isPinned !== b.isPinned) {
+ return a.isPinned ? -1 : 1
+ }
+ if ((b.usageCount || 0) !== (a.usageCount || 0)) {
+ return (b.usageCount || 0) - (a.usageCount || 0)
+ }
+ if (a.lastUsedAt && b.lastUsedAt) {
+ return new Date(b.lastUsedAt) - new Date(a.lastUsedAt)
+ }
+ return new Date(b.createdAt) - new Date(a.createdAt)
+ })
+ setSavedFilters(sortedFilters)
+ } catch (error) {
+ console.error('Error loading filters:', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ loadFilters()
+ }, [])
+
+ // Calculate task counts for each filter
+ useEffect(() => {
+ if (chores && chores.res && savedFilters.length > 0) {
+ const choresList = chores.res
+ const counts = {}
+
+ const context = {
+ userId: userProfile?.id,
+ members: membersData?.res || [],
+ labels: labels || [],
+ projects: projects || [],
+ }
+
+ savedFilters.forEach(filter => {
+ try {
+ const count = getFilterCount(choresList, filter, context)
+ const overdueCount = getFilterOverdueCount(
+ choresList,
+ filter,
+ context,
+ )
+ counts[filter.id] = { count, overdueCount }
+ } catch (error) {
+ console.error(
+ `Error calculating count for filter ${filter.id}:`,
+ error,
+ )
+ counts[filter.id] = { count: 0, overdueCount: 0 }
+ }
+ })
+
+ setFilterCounts(counts)
+ }
+ }, [
+ chores,
+ savedFilters,
+ userProfile?.id,
+ labels,
+ projects,
+ membersData?.res,
+ ])
+
+ const handleAddFilter = () => {
+ setEditingFilter(null)
+ setShowAdvancedFilterBuilder(true)
+ }
+
+ const handleEditFilter = filter => {
+ setEditingFilter(filter)
+ setShowAdvancedFilterBuilder(true)
+ }
+
+ const handleDeleteClicked = id => {
+ const filter = savedFilters.find(f => f.id === id)
+ setConfirmationModel({
+ isOpen: true,
+ title: 'Delete Filter',
+ message: `Are you sure you want to delete "${filter?.name}"? This cannot be undone.`,
+ confirmText: 'Delete',
+ color: 'danger',
+ cancelText: 'Cancel',
+ onClose: confirmed => {
+ if (confirmed === true) {
+ handleDeleteFilter(id)
+ }
+ setConfirmationModel({})
+ },
+ })
+ }
+
+ const handleDeleteFilter = id => {
+ try {
+ deleteFilter(id)
+ // if it's the selected filter, we might want to clear it
+ localStorage.getItem('selectedChoreFilter') === id &&
+ localStorage.removeItem('selectedChoreFilter')
+
+ loadFilters()
+ } catch (error) {
+ console.error('Error deleting filter:', error)
+ }
+ }
+
+ const handlePinFilter = id => {
+ try {
+ toggleFilterPin(id)
+ loadFilters()
+ } catch (error) {
+ console.error('Error pinning filter:', error)
+ }
+ }
+
+ const handleSaveFilter = filterData => {
+ try {
+ if (editingFilter) {
+ // Update existing filter
+ updateFilter(editingFilter.id, filterData)
+ } else {
+ // Save new filter
+ saveFilter(filterData)
+ }
+ setShowAdvancedFilterBuilder(false)
+ setEditingFilter(null)
+ loadFilters()
+ } catch (error) {
+ console.error('Error saving filter:', error)
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Filters
+
+
+ Save your favorite filter combinations for quick access. Create
+ custom views to organize and find tasks faster.
+
+
+
+
+
+ {savedFilters.length === 0 ? (
+
+
+
+ No saved filters yet
+
+
+ Create custom filters to quickly access your most used chore
+
+
+ ) : (
+ savedFilters.map(filter => (
+
+ ))
+ )}
+
+
+ {showAdvancedFilterBuilder && (
+ {
+ setShowAdvancedFilterBuilder(false)
+ setEditingFilter(null)
+ }}
+ onSave={handleSaveFilter}
+ members={membersData?.res || []}
+ labels={labels || []}
+ projects={projects || []}
+ allChores={chores?.res || []}
+ userProfile={userProfile}
+ editingFilter={editingFilter}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default FilterView
diff --git a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
index ae4cfc5..9be4f0a 100644
--- a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
+++ b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
@@ -9,10 +9,12 @@ import {
ListItem,
Option,
Select,
+ Textarea,
Typography,
} from '@mui/joy'
import { useEffect, useMemo, useState } from 'react'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
+import { FILTER_COLORS } from '../../../utils/Colors'
import { filterNameExists } from '../../../utils/CustomFilterStorage'
import { applyFilter } from '../../../utils/FilterEngine'
import Priorities from '../../../utils/Priorities'
@@ -30,19 +32,36 @@ const AdvancedFilterBuilder = ({
}) => {
const { ResponsiveModal } = useResponsiveModal()
const [filterName, setFilterName] = useState('')
+ const [filterDescription, setFilterDescription] = useState('')
+ const [filterColor, setFilterColor] = useState(FILTER_COLORS[0].value)
const [conditions, setConditions] = useState([
{ type: 'assignee', operator: 'is', value: [] },
])
const [error, setError] = useState('')
+ const [existedFilters] = useState(() => {
+ const storedFilters = localStorage.getItem('customFilters')
+ return storedFilters ? JSON.parse(storedFilters) : []
+ })
// Initialize state when editing a filter
useEffect(() => {
if (editingFilter) {
setFilterName(editingFilter.name)
+ setFilterDescription(editingFilter.description || '')
+ setFilterColor(editingFilter.color || FILTER_COLORS[0].value)
setConditions(editingFilter.conditions || [])
setError('')
} else {
setFilterName('')
+ setFilterDescription('')
+ // find color no filter has it :
+ const potentialColor = FILTER_COLORS.find(
+ color => !existedFilters.some(filter => filter.color === color.value),
+ )
+
+ setFilterColor(
+ potentialColor ? potentialColor.value : FILTER_COLORS[0].value,
+ )
setConditions([{ type: 'assignee', operator: 'is', value: [] }])
setError('')
}
@@ -127,6 +146,8 @@ const AdvancedFilterBuilder = ({
const filterData = {
name: filterName.trim(),
+ description: filterDescription.trim(),
+ color: filterColor,
conditions: validConditions,
operator: 'AND',
}
@@ -375,11 +396,27 @@ const AdvancedFilterBuilder = ({
}
return (
-
-
- {editingFilter ? 'Edit Filter' : 'Create Advanced Filter'}
-
-
+
+
+ }
+ >
+ {editingFilter ? 'Update Filter' : 'Save Filter'}
+
+
+ }
+ >
+
+
+ Description (Optional)
+
+
+
+
+
+ Color
+
+
+
+
-
-
-
- }
- >
- {editingFilter ? 'Update Filter' : 'Save Filter'}
-
-
)
diff --git a/src/views/Modals/Inputs/SaveFilterModal.jsx b/src/views/Modals/Inputs/SaveFilterModal.jsx
deleted file mode 100644
index 3d547ed..0000000
--- a/src/views/Modals/Inputs/SaveFilterModal.jsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import { Save, Star, StarBorder } from '@mui/icons-material'
-import {
- Box,
- Button,
- Chip,
- Input,
- Modal,
- ModalClose,
- ModalDialog,
- Typography,
-} from '@mui/joy'
-import { useState } from 'react'
-import { filterNameExists } from '../../../utils/CustomFilterStorage'
-import CompactChoreCard from '../../Chores/CompactChoreCard'
-
-const SaveFilterModal = ({
- isOpen,
- onClose,
- onSave,
- filterData,
- previewChores = [],
- previewCount = 0,
- previewOverdueCount = 0,
-}) => {
- const [filterName, setFilterName] = useState('')
- const [isPinned, setIsPinned] = useState(false)
- const [error, setError] = useState('')
-
- const handleSave = () => {
- if (!filterName.trim()) {
- setError('Please enter a filter name')
- return
- }
-
- if (filterNameExists(filterName.trim())) {
- setError('A filter with this name already exists')
- return
- }
-
- const newFilter = {
- ...filterData,
- name: filterName.trim(),
- isPinned,
- }
-
- onSave(newFilter)
- onClose()
- }
-
- const getConditionLabel = condition => {
- switch (condition.type) {
- case 'assignee':
- if (condition.value === 'me') return 'Assigned to me'
- if (condition.value === 'others') return 'Assigned to others'
- return 'Specific assignee'
-
- case 'createdBy':
- if (condition.value === 'me') return 'Created by me'
- return 'Created by specific user'
-
- case 'priority':
- return `Priority ${condition.value}`
-
- case 'status':
- return condition.value === 3 ? 'Pending approval' : `Status ${condition.value}`
-
- case 'dueDate':
- if (condition.operator === 'isOverdue') return 'Overdue'
- if (condition.operator === 'isDueToday') return 'Due today'
- if (condition.operator === 'isDueThisWeek') return 'Due this week'
- if (condition.operator === 'hasNoDueDate') return 'No due date'
- return 'Due date condition'
-
- case 'label':
- return 'Has label'
-
- case 'project':
- if (condition.value === 'default') return 'Default project'
- return 'Specific project'
-
- default:
- return condition.type
- }
- }
-
- return (
-
-
-
-
- Save Filter
-
-
-
-
-
- Filter Name
-
- {
- setFilterName(e.target.value)
- setError('')
- }}
- error={!!error}
- autoFocus
- />
- {error && (
-
- {error}
-
- )}
-
-
-
-
- Filter Conditions
-
-
- {filterData.conditions.length === 0 ? (
-
- No filters applied
-
- ) : (
- filterData.conditions.map((condition, index) => (
-
- {getConditionLabel(condition)}
-
- ))
- )}
-
-
-
-
-
- Preview
-
-
- {previewCount} tasks
-
- {previewOverdueCount > 0 && (
-
- {previewOverdueCount} overdue
-
- )}
-
-
-
-
- {previewCount === 0 ? (
-
- No tasks match these filters
-
- ) : (
-
- {previewChores.slice(0, 3).map(chore => (
-
- {chore.name}
-
- ))}
- {previewCount > 3 && (
-
- ...and {previewCount - 3} more
-
- )}
-
- )}
-
-
-
- setIsPinned(!isPinned)}
- >
- {isPinned ? (
-
- ) : (
-
- )}
-
-
- Pin this filter
-
-
- Pinned filters appear first in the list
-
-
-
-
-
-
- }
- disabled={!filterName.trim() || filterData.conditions.length === 0}
- >
- Save Filter
-
-
-
-
-
- )
-}
-
-export default SaveFilterModal
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index fc360e2..e25b226 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -2,6 +2,7 @@ import { Capacitor } from '@capacitor/core'
import {
Archive,
ArrowBack,
+ FilterAlt,
FolderOpen,
History,
Inbox,
@@ -60,6 +61,11 @@ const links = [
label: 'Projects',
icon: ,
},
+ {
+ to: 'filters',
+ label: 'Filters',
+ icon: ,
+ },
{
to: 'activities',
label: 'Activities',