import '@meauxt/react-swipeable-list/dist/styles.css' import { SwipeableList, SwipeableListItem, SwipeAction, TrailingActions, Type as ListType, } from '@meauxt/react-swipeable-list' import { Add, Close, MoreVert, Search, SearchOff, Task, } from '@mui/icons-material' import DeleteIcon from '@mui/icons-material/Delete' import EditIcon from '@mui/icons-material/Edit' import { Avatar, Box, Chip, CircularProgress, Container, IconButton, Input, Stack, Typography, } from '@mui/joy' import { useQueryClient } from '@tanstack/react-query' import Fuse from 'fuse.js' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useSearchParams } from 'react-router-dom' import EmptyState from '../../components/common/EmptyState' import SortAndFilterMenu from '../../components/common/SortAndFilterMenu' import { useChores } from '../../queries/ChoreQueries' import { useUserProfile } from '../../queries/UserQueries' import { getTextColorFromBackgroundColor } from '../../utils/Colors' import { DeleteProject } from '../../utils/Fetcher' import { getIconComponent } from '../../utils/ProjectIcons' import { getSafeBottomStyles } from '../../utils/SafeAreaUtils' import { useProjectFilter } from '../Chores/hooks/useProjectFilter' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import ProjectModal from '../Modals/Inputs/ProjectModal' import { useProjects } from './ProjectQueries' const ProjectCardContent = ({ currentUserId, onCardClick, onToggleActions, project, taskCounts = {}, }) => { const { t } = useTranslation('projects') // Check if current user owns this project const isOwnedByCurrentUser = project.created_by === currentUserId const isDefaultProject = project.id === 'default' const taskCount = taskCounts[project.id] || 0 return ( {/* Project Avatar */} {project.icon ? ( (() => { const IconComponent = getIconComponent(project.icon) return ( ) })() ) : ( <> )} {/* Content - Center */} {/* Project Name */} {project.name} {isDefaultProject && ( {t('defaultChip')} )} {/* Project Info */} {project.description && ( {project.description} )} } sx={{ fontSize: 10, height: 18, px: 0.75, bgcolor: 'primary.softBg', color: 'primary.500', }} > {t('tasks', { count: taskCount })} {!isOwnedByCurrentUser && !isDefaultProject && ( {t('shared')} )} {onToggleActions && ( { e.stopPropagation() onToggleActions() }} > )} ) } const ProjectView = () => { const { t } = useTranslation('projects') const { data: projects, isError, isProjectsLoading } = useProjects() const { data: userProfile } = useUserProfile() const { data: chores = { res: [] } } = useChores(false) // false to exclude archived const { data: projectsData = [], isLoading: projectsLoading } = useProjects() const { setSelectedProjectWithCache } = useProjectFilter( projectsData, !projectsLoading, ) const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const [userProjects, setUserProjects] = useState([]) const [modalOpen, setModalOpen] = useState(false) const [currentProject, setCurrentProject] = useState(null) const [taskCounts, setTaskCounts] = useState({}) const queryClient = useQueryClient() const [confirmationModel, setConfirmationModel] = useState({}) const [showMoreInfoId, setShowMoreInfoId] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [sortBy, setSortBy] = useState( () => localStorage.getItem('projectsSortBy') || 'name', ) const [sortDirection, setSortDirection] = useState( () => localStorage.getItem('projectsSortDirection') || 'asc', ) const [ownershipFilter, setOwnershipFilter] = useState('all') const searchInputRef = useRef(null) useEffect(() => { localStorage.setItem('projectsSortBy', sortBy) localStorage.setItem('projectsSortDirection', sortDirection) }, [sortBy, sortDirection]) const visibleProjects = useMemo(() => { if (ownershipFilter === 'mine') { return userProjects.filter( project => project.created_by === userProfile?.id, ) } if (ownershipFilter === 'shared') { return userProjects.filter( project => project.created_by !== userProfile?.id, ) } return userProjects }, [ownershipFilter, userProjects, userProfile?.id]) const fuse = useMemo( () => new Fuse(visibleProjects, { keys: ['name', 'description'], includeScore: true, isCaseSensitive: false, findAllMatches: true, }), [visibleProjects], ) const filteredProjects = useMemo(() => { const matched = searchTerm ? fuse.search(searchTerm).map(result => result.item) : visibleProjects const direction = sortDirection === 'desc' ? -1 : 1 return [...matched].sort((a, b) => { switch (sortBy) { case 'tasks': return direction * ((taskCounts[a.id] || 0) - (taskCounts[b.id] || 0)) case 'created': return direction * ((a.id || 0) - (b.id || 0)) case 'name': default: return direction * (a.name || '').localeCompare(b.name || '') } }) }, [fuse, searchTerm, visibleProjects, sortBy, sortDirection, taskCounts]) // The default project is pinned above the list, so it is matched separately. const showDefaultProject = useMemo(() => { if (ownershipFilter === 'shared') return false if (!searchTerm) return true return t('chores:toolbar.defaultProject') .toLowerCase() .includes(searchTerm.toLowerCase()) }, [ownershipFilter, searchTerm, t]) const handleSearchChange = e => { setSearchTerm(e.target.value) setShowMoreInfoId(null) } const handleSearchClose = () => { setSearchTerm('') searchInputRef.current?.blur() } const handleAddProject = () => { setCurrentProject(null) setModalOpen(true) } const handleEditProject = project => { setCurrentProject(project) setModalOpen(true) } const handleDeleteClicked = id => { const project = userProjects.find(p => p.id === id) setConfirmationModel({ isOpen: true, title: t('delete.title'), message: t('delete.message', { name: project?.name }), confirmText: t('common:delete'), color: 'danger', cancelText: t('common:cancel'), onClose: confirmed => { if (confirmed === true) { handleDeleteProject(id) } setConfirmationModel({}) }, }) } const handleDeleteProject = id => { DeleteProject(id).then(() => { const updatedProjects = userProjects.filter(project => project.id !== id) setUserProjects(updatedProjects) queryClient.invalidateQueries('projects') // If the deleted project was the active project, clear it const saved = localStorage.getItem('selectedProject') if (saved) { const savedProject = JSON.parse(saved) if (savedProject && savedProject.id === id) { setSelectedProjectWithCache(null) } } }) } const handleSaveProject = () => { setModalOpen(false) } const handleCardClick = project => { // Always navigate to MyChores with project filter when clicking on the card // For default project, use 'default', for others use project ID const projectIdentifier = project.id === 'default' ? 'default' : project.id setSelectedProjectWithCache(project) navigate(`/chores?project=${encodeURIComponent(projectIdentifier)}`) } useEffect(() => { if (projects) { setUserProjects(projects) } }, [projects]) // ?create=1 lets other surfaces (global search quick actions) land here with // the create modal already open. useEffect(() => { if (searchParams.get('create') !== '1') return setCurrentProject(null) setModalOpen(true) setSearchParams( params => { params.delete('create') return params }, { replace: true }, ) }, [searchParams, setSearchParams]) // Calculate real task counts from chores data useEffect(() => { if (chores && chores.res) { const choresList = chores.res const realCounts = {} // First, count tasks for the default project (tasks without a projectId) const defaultProjectCount = choresList.filter(chore => { const choreProjectId = chore.projectId || chore.project_id return ( !choreProjectId || choreProjectId === '' || choreProjectId === 'default' || choreProjectId === null ) }).length realCounts['default'] = defaultProjectCount // Then count tasks for each user project userProjects.forEach(project => { const choreCount = choresList.filter(chore => { const choreProjectId = chore.projectId || chore.project_id return choreProjectId === project.id }).length realCounts[project.id] = choreCount }) setTaskCounts(realCounts) } }, [chores, userProjects]) if (isProjectsLoading) { return ( ) } if (isError) { return ( {t('loadError')} ) } return ( {t('common:navigation.projects')} {t('blurb')} } endDecorator={ searchTerm && ( ) } /> { setOwnershipFilter(value) setShowMoreInfoId(null) }} isActive={ ownershipFilter !== 'all' || sortBy !== 'name' || sortDirection !== 'asc' } /> {!showDefaultProject && filteredProjects.length === 0 && ( } title={t('search.noResultsTitle')} description={ searchTerm ? t('search.noResultsDescription', { searchTerm }) : t('search.noFilterResultsDescription') } primaryAction={{ label: searchTerm ? t('search.clear') : t('search.showAll'), onClick: () => { handleSearchClose() setOwnershipFilter('all') }, }} /> )} {/* Default project - not swipeable */} {showDefaultProject && ( handleCardClick({ id: 'default', name: t('chores:toolbar.defaultProject'), icon: 'FolderOpen', color: '#1976d2', }) } /> )} {/* User projects - swipeable */} {filteredProjects.map(project => ( handleCardClick(project)} key={project.id} swipeActionOpen={ showMoreInfoId === project.id ? 'trailing' : null } trailingActions={ handleEditProject(project)}> {t('common:edit')} handleDeleteClicked(project.id)} > {t('common:delete')} } > { if (showMoreInfoId === project.id) { setShowMoreInfoId(null) } else { setShowMoreInfoId(project.id) } }} /> ))} {modalOpen && ( setModalOpen(false)} onSave={handleSaveProject} project={currentProject} /> )} ) } export default ProjectView