From 23e777cd94284ff6dece5e63e288474f04d6a412 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 14 Jun 2026 15:54:40 -0400 Subject: [PATCH] implement unified ChoreToolbar and enhance ChoreHistory with filtering options --- src/views/Chores/MyChores.jsx | 215 ++--- .../components/ChoreToolbarPrototype.jsx | 732 ++++++++++++++++++ src/views/History/ChoreHistory.jsx | 132 +++- 3 files changed, 915 insertions(+), 164 deletions(-) create mode 100644 src/views/Chores/components/ChoreToolbarPrototype.jsx diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index cbb689b..3554091 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -2,15 +2,10 @@ import { Add, Bolt, CalendarMonth, - CheckBox, - CheckBoxOutlineBlank, EditCalendar, ExpandCircleDown, PriorityHigh, - Sort, Style, - ViewAgenda, - ViewModule, } from '@mui/icons-material' import { Accordion, @@ -37,7 +32,6 @@ import IconButtonWithMenu from './IconButtonWithMenu' import { useMediaQuery } from '@mui/material' import { useQueryClient } from '@tanstack/react-query' -import FilterBar from '../../components/common/FilterBar' import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint' import { useFilter } from '../../hooks/useFilter' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' @@ -52,15 +46,13 @@ import { getSafeBottom } from '../../utils/SafeAreaUtils.js' import TaskInput from '../components/AddTaskModal' import CalendarDual from '../components/CalendarDual' import CalendarMonthly from '../components/CalendarMonthly.jsx' -import ProjectSelector from '../components/ProjectSelector' import AdvancedFilterBuilder from '../Modals/Inputs/AdvancedFilterBuilder' import { useProjects } from '../Projects/ProjectQueries.js' import ChoreListView from './ChoreListView.jsx' +import ChoreToolbar from './components/ChoreToolbarPrototype' import ChoreModals from './components/ChoreModals' -import FilterSection from './components/FilterSection' import MultiSelectToolbar from './components/MultiSelectToolbar' import MyChoreHeader from './components/MyChoreHeader' -import SearchBar from './components/SearchBar' import { useChoreActions } from './hooks/useChoreActions' import { useChoreFilters } from './hooks/useChoreFilters' import { useChoreModals } from './hooks/useChoreModals' @@ -75,7 +67,6 @@ import { import NotificationAccessSnackbar from './NotificationAccessSnackbar' import Sidepanel from './Sidepanel' import { INSIGHT_FILTER_DEFS } from './SmartInsightsCard' -import SortAndGrouping from './SortAndGrouping' const MyChores = () => { const { data: userProfile, isLoading: isUserProfileLoading } = @@ -790,14 +781,13 @@ const MyChores = () => { localStorage.setItem('openChoreSections', JSON.stringify(value)) } - const toggleViewMode = () => { - const modes = ['default', 'compact', 'calendar'] - const currentIndex = modes.indexOf(viewMode) - const nextIndex = (currentIndex + 1) % modes.length - const newMode = modes[nextIndex] + const toggleViewMode = value => { + const newMode = value ?? (() => { + const modes = ['default', 'compact', 'calendar'] + return modes[(modes.indexOf(viewMode) + 1) % modes.length] + })() setViewMode(newMode) localStorage.setItem('choreCardViewMode', newMode) - if (newMode !== 'calendar') { setSelectedCalendarDate(null) } @@ -957,126 +947,7 @@ const MyChores = () => { tempFilter={tempFilter} tempFilterMeta={tempFilterMeta} /> - - - - } - selectedItem={selectedChoreSection} - selectedFilter={selectedChoreFilter} - setFilter={filter => { - setSelectedChoreFilterWithCache(filter) - // Clear active custom filter when quick filter is applied - if (activeFilterId) { - clearActiveFilter() - updateFilterUrl(null, null) - } - }} - onItemSelect={selected => { - setSelectedChoreSectionWithCache(selected.value) - setFilteredChores(chores) - clearQuickFilters() - }} - onCreateNewFilter={() => { - setShowAdvancedFilterBuilder(true) - setEditingFilter(null) - }} - mouseClickHandler={handleMenuOutsideClick} - /> - - {/* Project Selector - Hidden when active filter has project conditions */} - {projectsWithDefault.length > 1 && - !hasProjectConditions && - !hasFilterApplied && ( - { - setSelectedProjectWithCache(project) - clearActiveFilter() - }} - showKeyboardShortcuts={showKeyboardShortcuts} - /> - )} - - {/* View Mode Toggle Button */} - - {viewMode === 'default' ? ( - - ) : viewMode === 'compact' ? ( - - ) : ( - - )} - - - {/* Multi-select Toggle Button */} - - - {isMultiSelectMode ? : } - - - - - - {/* Quick Filters */} - { @@ -1084,21 +955,40 @@ const MyChores = () => { setQuickFilter(id, value) setSelectedCalendarDate(null) }} - onClearAll={() => { + onClearAllFilters={() => { clearQuickFilters() + clearActiveFilter() + setSelectedChoreFilterWithCache('anyone') + setSelectedProjectWithCache(projectsWithDefault.find(p => p.id === 'default') || null) updateFilterUrl(null, null) }} - resultCount={hasQuickFilters ? getFilteredChores.length : undefined} - totalCount={hasQuickFilters ? projectFilteredChores.length : undefined} - /> - - {/* Custom Filters Section */} - { + setSelectedProjectWithCache(project) + clearActiveFilter() + }} + selectedAssigneeFilter={selectedChoreFilter} + onAssigneeFilterChange={filter => { + setSelectedChoreFilterWithCache(filter) + if (activeFilterId) { + clearActiveFilter() + updateFilterUrl(null, null) + } + }} savedFilters={savedFilters} activeFilterId={activeFilterId} - activeFilter={activeFilter} - hasProjectConditions={hasProjectConditions} - onFilterClick={filterId => { + onSavedFilterClick={filterId => { if (activeFilterId === filterId) { clearActiveFilter() updateFilterUrl(null, null) @@ -1106,31 +996,42 @@ const MyChores = () => { clearQuickFilters() setSearchTerm('') setFilteredChores([]) - - // Reset quick filter to 'anyone' when custom filter is applied if (selectedChoreFilter !== 'anyone') { setSelectedChoreFilterWithCache('anyone') } - - // Clear project selection if the filter has project conditions const filter = savedFilters.find(f => f.id === filterId) if (filter?.conditions?.some(c => c.type === 'project')) { setSelectedProjectWithCache(null) } - applyCustomFilter(filterId) updateFilterUrl('filterId', filterId) } }} - onFilterDelete={deleteFilter} - onFilterPin={pinFilter} - onFilterEdit={filter => { + onSavedFilterEdit={filter => { setEditingFilter(filter) setShowAdvancedFilterBuilder(true) }} - onClearActiveFilter={clearActiveFilter} - onCreateAdvancedFilter={() => setShowAdvancedFilterBuilder(true)} - updateFilterUrl={updateFilterUrl} + onSavedFilterDelete={deleteFilter} + onSavedFilterPin={pinFilter} + onCreateAdvancedFilter={() => { + setShowAdvancedFilterBuilder(true) + setEditingFilter(null) + }} + selectedGroupBy={selectedChoreSection} + onGroupBySelect={value => { + setSelectedChoreSectionWithCache(value) + setFilteredChores(chores) + clearQuickFilters() + }} + viewMode={viewMode} + onToggleViewMode={toggleViewMode} + isMultiSelectMode={isMultiSelectMode} + onToggleMultiSelect={toggleMultiSelectMode} + searchTerm={searchTerm} + onSearchChange={handleSearchChange} + onSearchClose={handleSearchClose} + searchInputRef={searchInputRef} + showKeyboardShortcuts={showKeyboardShortcuts} /> toolbar block + * and the two rows below it (FilterBar + FilterSection) with: + * + */ + +import { + Add, + CalendarMonth, + Check, + CheckBox, + CheckBoxOutlineBlank, + Close, + FilterList, + FolderOpen, + PriorityHigh, + Settings, + Sort, + Style, + Tune, + ViewAgenda, + ViewComfy, + ViewModule, +} from '@mui/icons-material' +import { + Badge, + Box, + Button, + Chip, + Divider, + IconButton, + Typography, +} from '@mui/joy' +import { useState } from 'react' +import BottomSheetModal from '../../../components/common/BottomSheetModal' +import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint' +import SearchBar from './SearchBar' + +// ─── helpers ───────────────────────────────────────────────────────────────── + +const chipLabel = (def, value) => { + if (!value || (Array.isArray(value) && value.length === 0)) return null + if (def.type === 'single-select') + return def.options?.find(o => o.value === value)?.label ?? null + if (def.type === 'multi-select' && Array.isArray(value)) { + if (value.length === 1) + return def.options?.find(o => o.value === value[0])?.label ?? def.label + return `${def.label} (${value.length})` + } + return null +} + +// ─── sub-components ────────────────────────────────────────────────────────── + +const SectionHeader = ({ icon, label, badge }) => ( + + {icon && ( + + {icon} + + )} + + {label} + + {badge != null && ( + + {badge} + + )} + +) + +const OptionChips = ({ options, selected, multi, onToggle }) => ( + + {options.map(opt => { + const isSelected = multi + ? (selected || []).includes(opt.value) + : selected === opt.value + return ( + + : opt.icon + : undefined + } + onClick={() => onToggle(opt.value)} + sx={{ + py: 0.64, + cursor: 'pointer', + transition: 'all 0.15s ease', + userSelect: 'none', + '&:hover': { opacity: 0.85 }, + }} + > + {opt.label} + + ) + })} + +) + +// ─── main component ─────────────────────────────────────────────────────────── + +/** + * Props: + * filterDefs – same shape as FilterBar: [{ id, label, type, icon, options, filterFn }] + * PLUS two new built-in sections handled here: 'project' and 'assignee' + * activeFilters – { [id]: value } + * onSetFilter – (id, value | null) => void + * onClearAllFilters – () => void + * resultCount / totalCount + * + * projects – [{ id, name }] — drives the Project section + * selectedProject – current project object + * onProjectSelect – (project) => void + * + * selectedAssigneeFilter – 'anyone' | 'assigned_to_me' | 'available_for_me' | 'assigned_to_others' + * onAssigneeFilterChange – (key) => void + * + * savedFilters – [{ id, name, color, count, isPinned }] + * activeFilterId – number | null + * onSavedFilterClick – (id) => void + * onSavedFilterEdit – (filter) => void + * onSavedFilterDelete– (id) => void + * onSavedFilterPin – (id) => void + * onCreateAdvancedFilter – () => void + * + * selectedGroupBy – 'default' | 'due_date' | 'priority' | 'labels' + * onGroupBySelect – (value) => void + * + * viewMode – 'default' | 'compact' | 'calendar' + * onToggleViewMode – () => void + * + * isMultiSelectMode – bool + * onToggleMultiSelect – () => void + * + * searchTerm / onSearchChange / onSearchClose / searchInputRef + * showKeyboardShortcuts + */ +const ChoreToolbar = ({ + // quick filters + filterDefs = [], + activeFilters = {}, + onSetFilter, + onClearAllFilters, + resultCount, + totalCount, + // project + projects = [], + selectedProject, + onProjectSelect, + // assignee + selectedAssigneeFilter = 'anyone', + onAssigneeFilterChange, + // saved / custom + savedFilters = [], + activeFilterId, + onSavedFilterClick, + onSavedFilterEdit, + onSavedFilterDelete, + onSavedFilterPin, + onCreateAdvancedFilter, + // grouping + selectedGroupBy = 'default', + onGroupBySelect, + // view + multiselect + viewMode = 'default', + onToggleViewMode, + isMultiSelectMode, + onToggleMultiSelect, + // search + searchTerm, + onSearchChange, + onSearchClose, + searchInputRef, + showKeyboardShortcuts, +}) => { + const [filterSheetOpen, setFilterSheetOpen] = useState(false) + const [displaySheetOpen, setDisplaySheetOpen] = useState(false) + + // ── count active filters for badge ────────────────────────────────────────── + + const quickFilterCount = filterDefs.filter(def => { + const v = activeFilters[def.id] + return v != null && !(Array.isArray(v) && v.length === 0) + }).length + + const projectActive = + selectedProject && selectedProject.id !== 'default' ? 1 : 0 + const assigneeActive = selectedAssigneeFilter !== 'anyone' ? 1 : 0 + const savedFilterActive = activeFilterId != null ? 1 : 0 + + const totalActiveCount = quickFilterCount + savedFilterActive + const hasAnyActive = totalActiveCount > 0 + + // ── inline chip strip (max 2 visible + overflow) ───────────────────────────── + + const inlineChips = [] + + if (savedFilterActive) { + const sf = savedFilters.find(f => f.id === activeFilterId) + if (sf) + inlineChips.push({ + key: '__saved', + label: sf.name, + onClear: () => onSavedFilterClick?.(activeFilterId), + }) + } + + filterDefs.forEach(def => { + const label = chipLabel(def, activeFilters[def.id]) + if (label) inlineChips.push({ key: def.id, label, onClear: () => onSetFilter(def.id, null) }) + }) + + const MAX_CHIPS = 2 + const visibleChips = inlineChips.slice(0, MAX_CHIPS) + const overflow = inlineChips.length - MAX_CHIPS + + // ── groupby options ────────────────────────────────────────────────────────── + + const groupByOptions = [ + { value: 'default', label: 'Smart' }, + { value: 'due_date', label: 'Due Date' }, + { value: 'priority', label: 'Priority' }, + { value: 'labels', label: 'Labels' }, + ] + + // ── assignee options ───────────────────────────────────────────────────────── + + const assigneeOptions = [ + { value: 'anyone', label: 'Everyone' }, + { value: 'assigned_to_me', label: 'Mine' }, + { value: 'available_for_me', label: 'Available to me' }, + { value: 'assigned_to_others', label: 'Others' }, + ] + + // ── project options (show only if more than just Default) ─────────────────── + + const showProjectSection = projects.filter(p => p.id !== 'default').length > 0 + + // ── pinned saved filters ───────────────────────────────────────────────────── + + const pinnedFilters = savedFilters.filter(f => f.isPinned) + + // ── display active state (highlight button when non-default) ───────────────── + + const displayActive = + selectedGroupBy !== 'default' || + viewMode !== 'default' || + projectActive > 0 || + assigneeActive > 0 + + const viewOptions = [ + { value: 'default', label: 'Cards', icon: }, + { value: 'compact', label: 'Compact', icon: }, + { value: 'calendar', label: 'Calendar', icon: }, + ] + + return ( + <> + {/* ── Row 1: main toolbar ─────────────────────────────────────────────── */} + + {/* Search takes available space */} + + + {/* Filter button */} + + setFilterSheetOpen(true)} + title='Filters' + > + + + + + {/* Display button — View + Group combined */} + setDisplaySheetOpen(true)} + title='View & Group' + > + {viewMode === 'calendar' ? ( + + ) : viewMode === 'compact' ? ( + + ) : ( + + )} + + + {/* Multiselect */} + + + {isMultiSelectMode ? : } + + + + + + {/* ── Row 2: active filter chips (only when something is active) ─────── */} + {hasAnyActive && ( + + {visibleChips.map(({ key, label, onClear }) => ( + { + e.stopPropagation() + onClear() + }} + /> + } + onClick={() => setFilterSheetOpen(true)} + sx={{ cursor: 'pointer', flexShrink: 0 }} + > + {label} + + ))} + + {overflow > 0 && ( + setFilterSheetOpen(true)} + sx={{ cursor: 'pointer', flexShrink: 0 }} + > + +{overflow} more + + )} + + {resultCount != null && totalCount != null && ( + + {resultCount} / {totalCount} + + )} + + + + )} + + {/* ── Unified Filter bottom sheet ─────────────────────────────────────── */} + setFilterSheetOpen(false)} + title={ + + + Filters + {hasAnyActive && ( + + {totalActiveCount} + + )} + + } + footer={ + + + + + } + > + + + {/* ── Quick filter sections (Due Date / Priority / Labels etc.) ────── */} + {filterDefs.map(def => ( + + + + { + if (def.type === 'multi-select') { + const curr = activeFilters[def.id] || [] + const next = curr.includes(val) + ? curr.filter(v => v !== val) + : [...curr, val] + onSetFilter(def.id, next.length > 0 ? next : null) + } else { + onSetFilter(def.id, activeFilters[def.id] === val ? null : val) + } + }} + /> + + ))} + + {/* ── Saved / custom filters ──────────────────────────────────────── */} + {(pinnedFilters.length > 0 || savedFilters.length > 0) && ( + <> + + + + Saved Filters + + { + setFilterSheetOpen(false) + // navigate to /filters or open settings + }} + > + + + + + {savedFilters.map(filter => { + const isActive = activeFilterId === filter.id + return ( + + ) : ( + + {filter.count ?? 0} + + ) + } + onClick={() => { + onSavedFilterClick?.(filter.id) + setFilterSheetOpen(false) + }} + sx={{ + cursor: 'pointer', + transition: 'all 0.15s ease', + userSelect: 'none', + '&:hover': { opacity: 0.85 }, + ...(filter.color && !isActive + ? { borderColor: filter.color } + : {}), + }} + > + {filter.name} + + ) + })} + + + + + )} + + {/* Edge case: no saved filters yet → still show "Create" CTA */} + {savedFilters.length === 0 && ( + <> + + + + )} + + + + {/* ── Display bottom sheet (View + Group combined) ───────────────────── */} + setDisplaySheetOpen(false)} + title={ + + + Display + + } + footer={ + + } + > + + {/* View section */} + + + {viewOptions.map(opt => ( + : opt.icon} + onClick={() => onToggleViewMode?.(opt.value)} + sx={{ + py: 0.64, + cursor: 'pointer', + transition: 'all 0.15s ease', + userSelect: 'none', + '&:hover': { opacity: 0.85 }, + }} + > + {opt.label} + + ))} + + + + + {/* Group by section */} + } + label='Group by' + badge={ + selectedGroupBy !== 'default' + ? groupByOptions.find(o => o.value === selectedGroupBy)?.label + : null + } + /> + + {groupByOptions.map(opt => ( + onGroupBySelect?.(opt.value)} + sx={{ + py: 0.64, + cursor: 'pointer', + transition: 'all 0.15s ease', + userSelect: 'none', + '&:hover': { opacity: 0.85 }, + }} + > + {opt.label} + + ))} + + + {/* Show tasks for section */} + + } + label='Show tasks for' + badge={ + selectedAssigneeFilter !== 'anyone' + ? assigneeOptions.find(o => o.value === selectedAssigneeFilter)?.label + : null + } + /> + onAssigneeFilterChange?.(v)} + /> + + {/* Project section */} + {showProjectSection && ( + <> + + } + label='Project' + badge={projectActive ? selectedProject.name : null} + /> + ({ 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) + }} + /> + + )} + + + + ) +} + +export default ChoreToolbar diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 17ed410..458b1f1 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -8,11 +8,21 @@ import { import '@meauxt/react-swipeable-list/dist/styles.css' import { Analytics, + CalendarMonth, + Check, Checklist, EventBusy, + EventNote, + FilterList, Group, History, + HourglassEmpty, + Person, + Redo, + RunningWithErrors, + Schedule, Star, + ThumbDown, Timelapse, TrendingUp, } from '@mui/icons-material' @@ -20,10 +30,12 @@ import DeleteIcon from '@mui/icons-material/Delete' import EditIcon from '@mui/icons-material/Edit' import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy' import moment from 'moment' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' +import FilterBar from '../../components/common/FilterBar' import { useLocalization } from '../../contexts/LocalizationContext' import useConfirmationModal from '../../hooks/useConfirmationModal' +import { useFilter } from '../../hooks/useFilter' import { useChoreHistory, useDeleteChoreHistory, @@ -34,6 +46,7 @@ import { useNotification } from '../../service/NotificationProvider' import { ChoreHistoryStatus } from '../../utils/Chores' import LoadingComponent from '../components/Loading' import EditHistoryModal from '../Modals/EditHistoryModal' +import HistoryDetailModal from '../Modals/HistoryDetailModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import NoteViewerModal from '../Modals/Inputs/NoteViewerModal' import HistoryCard from './HistoryCard' @@ -48,6 +61,7 @@ const ChoreHistory = () => { const { fmt } = useLocalization() const [showMoreInfoId, setShowMoreInfoId] = useState(null) const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false }) + const [detailModalConfig, setDetailModalConfig] = useState({ isOpen: false }) const { showSuccess, showError } = useNotification() // React Query hooks const { data: choreHistoryData, isLoading } = useChoreHistory(choreId) @@ -58,6 +72,61 @@ const ChoreHistory = () => { const choreHistory = choreHistoryData?.res || [] const performers = circleMembersData?.res || [] + const filterDefs = useMemo( + () => [ + { + id: 'status', + label: 'Status', + type: 'multi-select', + icon: , + options: [ + { value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: }, + { value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: }, + { value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: }, + { value: ChoreHistoryStatus.REJECTED, 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), + }, + { + id: 'hasNotes', + label: 'Has Notes', + type: 'boolean', + icon: , + filterFn: item => !!item.notes, + }, + { + id: 'completedBy', + label: 'Completed By', + type: 'multi-select', + icon: , + options: performers.map(p => ({ + value: p.userId, + label: p.displayName, + avatar: p.image, + })), + filterFn: (item, values) => values.includes(item.completedBy), + }, + { + id: 'dateRange', + label: 'Completed At', + type: 'date-range', + icon: , + filterFn: (item, value) => { + const performed = new Date(item.performedAt || item.updatedAt) + if (value.from && performed < new Date(value.from)) return false + if (value.to && performed > new Date(value.to)) return false + return true + }, + }, + ], + [performers], + ) + + const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } = + useFilter(choreHistory, filterDefs) + const handleDelete = historyEntry => { showConfirmation( `Are you sure you want to delete this history record?`, @@ -205,9 +274,10 @@ const ChoreHistory = () => { } return ( - + {/* Enhanced Header Section */} - + + {/* */} {/* Statistics Cards Grid - Compact Design */} @@ -285,7 +355,9 @@ const ChoreHistory = () => { {/* History Section Header */} - + + + { Task Activity + + + + + {filteredHistory.length === 0 && activeFilterCount > 0 && ( + + + + No results match your filters + + + Try adjusting or clearing the active filters. + + + + )} + + {filteredHistory.length > 0 && ( {/* Chore History List (Updated Style) */} - {choreHistory.map((historyEntry, index) => ( + {filteredHistory.map((historyEntry, index) => ( { performers={performers} allHistory={choreHistory} index={index} + onViewDetails={() => { + setDetailModalConfig({ + isOpen: true, + entry: historyEntry, + performers, + onClose: () => setDetailModalConfig({ isOpen: false }), + }) + }} onViewNote={notes => { setNoteViewerConfig({ isOpen: true, @@ -387,6 +503,7 @@ const ChoreHistory = () => { ))} + )} { /> - + + ) }