import React, { useState, useEffect, useMemo, useRef } from 'react'; import { MagnifyingGlassIcon, Squares2X2Icon, Bars3Icon, } from '@heroicons/react/24/solid'; import ConfirmDialog from './Shared/ConfirmDialog'; import ProjectModal from './Project/ProjectModal'; import SortFilter from './Shared/SortFilter'; import FilterDropdown, { FilterOption } from './Shared/FilterDropdown'; import { useStore } from '../store/useStore'; import { fetchProjects, createProject, updateProject, deleteProject, } from '../utils/projectsService'; import { fetchAreas } from '../utils/areasService'; import { useTranslation } from 'react-i18next'; import { SortOption } from './Shared/SortFilterButton'; import { Project } from '../entities/Project'; import { useSearchParams } from 'react-router-dom'; import ProjectItem from './Project/ProjectItem'; import ProjectShareModal from './Project/ProjectShareModal'; import { useToast } from './Shared/ToastContext'; const Projects: React.FC = () => { const { t } = useTranslation(); const { showErrorToast } = useToast(); const { areas, setAreas, setError: setAreasError, } = useStore((state) => state.areasStore); const { projects, setProjects, setLoading: setProjectsLoading, setError: setProjectsError, } = useStore((state) => state.projectsStore); const { isLoading, isError } = useStore((state) => state.projectsStore); // Try using a ref to avoid React state conflicts const modalStateRef = useRef({ isOpen: false, projectToEdit: null as Project | null, }); const [modalState, setModalState] = useState({ isOpen: false, projectToEdit: null as Project | null, }); const [projectToDelete, setProjectToDelete] = useState( null ); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [shareModal, setShareModal] = useState<{ isOpen: boolean; project: Project | null; }>({ isOpen: false, project: null }); const [activeDropdown, setActiveDropdown] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [viewMode, setViewMode] = useState<'cards' | 'list'>(() => { const saved = localStorage.getItem('projectsViewMode'); return saved === 'list' || saved === 'cards' ? saved : 'cards'; }); const [isSearchExpanded, setIsSearchExpanded] = useState(false); const [orderBy, setOrderBy] = useState('created_at:desc'); const [searchParams, setSearchParams] = useSearchParams(); const statusFilter = searchParams.get('status') || 'not_completed'; // Get area UID from URL parameters const getAreaUidFromParams = () => { const areaParam = searchParams.get('area'); if (areaParam) { // Extract area UID from the area parameter (format: uid-name-slug or just uid) return areaParam.split('-')[0]; } return ''; }; // Sort options for the filter button const sortOptions: SortOption[] = [ { value: 'created_at:desc', label: t('sort.created_at', 'Created At') }, { value: 'name:asc', label: t('sort.name', 'Name') }, { value: 'due_date_at:asc', label: t('sort.due_date', 'Due Date') }, { value: 'updated_at:desc', label: t('common.updated', 'Updated') }, ]; // Filter options for dropdowns const statusOptions: FilterOption[] = [ { value: 'all', label: t('projects.filters.all') }, { value: 'not_started', label: t('projectStatus.not_started', 'Not Started'), }, { value: 'planned', label: t('projectStatus.planned', 'Planned') }, { value: 'in_progress', label: t('projectStatus.in_progress', 'In Progress'), }, { value: 'waiting', label: t('projectStatus.waiting', 'Waiting') }, { value: 'done', label: t('projectStatus.done', 'Completed') }, { value: 'cancelled', label: t('projectStatus.cancelled', 'Cancelled'), }, { value: 'divider', label: '' }, { value: 'not_completed', label: t('projects.filters.notCompleted', 'Not Completed'), }, ]; const areaOptions: FilterOption[] = [ { value: '', label: t('projects.filters.allAreas') }, ...areas.map((area) => ({ value: area.uid, label: area.name, })), ]; useEffect(() => { const loadAreas = async () => { try { const areasData = await fetchAreas(); setAreas(areasData); } catch (error) { console.error('Failed to fetch areas:', error); setAreasError(true); } }; loadAreas(); }, []); // Persist viewMode to localStorage useEffect(() => { localStorage.setItem('projectsViewMode', viewMode); }, [viewMode]); // Projects are now loaded by Layout component into global store // Modal state tracking removed after fixing the issue // Handle click outside to close dropdown and escape key useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (activeDropdown === null) return; const target = event.target as Element; // Check if clicking inside any dropdown container const isInsideDropdown = target.closest('.dropdown-container'); if (!isInsideDropdown) { setActiveDropdown(null); } }; const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === 'Escape' && activeDropdown !== null) { setActiveDropdown(null); } }; if (activeDropdown !== null) { document.addEventListener('click', handleClickOutside, true); document.addEventListener('keydown', handleEscapeKey); } return () => { document.removeEventListener('click', handleClickOutside, true); document.removeEventListener('keydown', handleEscapeKey); }; }, [activeDropdown]); // Handle sort change const handleSortChange = (newOrderBy: string) => { setOrderBy(newOrderBy); }; const handleSaveProject = async (project: Project) => { setProjectsLoading(true); try { if (project.uid) { await updateProject(project.uid, project); } else { await createProject(project); } // Fetch all projects without filters to keep global store complete const projectsData = await fetchProjects('all', ''); setProjects(projectsData); } catch (error) { console.error('Error saving project:', error); setProjectsError(true); } finally { setProjectsLoading(false); setModalState({ isOpen: false, projectToEdit: null }); } }; const handleEditProject = (project: Project) => { modalStateRef.current = { isOpen: true, projectToEdit: project, }; setModalState({ isOpen: true, projectToEdit: project, }); }; const handleDeleteProject = async () => { if (!projectToDelete) return; try { if (projectToDelete.uid !== undefined) { setProjectsLoading(true); await deleteProject(projectToDelete.uid); // Fetch all projects without filters to keep global store complete const projectsData = await fetchProjects('all', ''); setProjects(projectsData); } else { console.error('Cannot delete project: UID is undefined.'); } } catch (error: any) { console.error('Error deleting project:', error); // Show permission denied if 403-like message, else generic const msg = typeof error?.message === 'string' && /403|Forbidden|permission/i.test(error.message) ? t('errors.permissionDenied', 'Permission denied') : t('projects.deleteError', 'Failed to delete project'); showErrorToast(msg); } finally { setProjectsLoading(false); setIsConfirmDialogOpen(false); setProjectToDelete(null); } }; const getCompletionPercentage = (project: Project) => { // Now the completion percentage comes directly from the backend return (project as any).completion_percentage || 0; }; const handleStatusFilterChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value === 'not_completed') { params.delete('status'); } else { params.set('status', value); } setSearchParams(params); }; const handleAreaFilterChange = (value: string) => { const params = new URLSearchParams(searchParams); params.delete('area'); if (value !== '') { params.set('area', value); } setSearchParams(params); }; // Update the area filter when areas are loaded (to handle area UID lookups) const actualAreaFilter = useMemo(() => { return getAreaUidFromParams(); }, [searchParams, areas]); // Filter, sort and search projects const displayProjects = useMemo(() => { let filteredProjects = [...projects]; // Apply status filter if (statusFilter === 'not_completed') { filteredProjects = filteredProjects.filter( (project) => project.status !== 'done' && project.status !== 'cancelled' ); } else if (statusFilter !== 'all') { filteredProjects = filteredProjects.filter( (project) => project.status === statusFilter ); } // Apply area filter by UID if (actualAreaFilter) { filteredProjects = filteredProjects.filter((project) => { const projectArea = project.area || (project as any).Area; return projectArea?.uid === actualAreaFilter; }); } // Apply search filter if (searchQuery.trim()) { filteredProjects = filteredProjects.filter( (project) => project.name .toLowerCase() .includes(searchQuery.toLowerCase()) || (project.description && project.description .toLowerCase() .includes(searchQuery.toLowerCase())) ); } // Apply sorting filteredProjects.sort((a, b) => { const [field, direction] = orderBy.split(':'); const isAsc = direction === 'asc'; let valueA, valueB; switch (field) { case 'name': valueA = a.name?.toLowerCase() || ''; valueB = b.name?.toLowerCase() || ''; break; case 'due_date_at': valueA = a.due_date_at ? new Date(a.due_date_at).getTime() : 0; valueB = b.due_date_at ? new Date(b.due_date_at).getTime() : 0; break; case 'updated_at': valueA = a.updated_at ? new Date(a.updated_at).getTime() : 0; valueB = b.updated_at ? new Date(b.updated_at).getTime() : 0; break; case 'created_at': default: valueA = a.created_at ? new Date(a.created_at).getTime() : 0; valueB = b.created_at ? new Date(b.created_at).getTime() : 0; break; } if (valueA < valueB) return isAsc ? -1 : 1; if (valueA > valueB) return isAsc ? 1 : -1; return 0; }); return filteredProjects; }, [projects, statusFilter, actualAreaFilter, searchQuery, orderBy]); if (isLoading) { return (
{t('projects.loading')}
); } if (isError) { return (
{t('projects.error')}
); } return (

{t('projects.title')}

{/* View Mode and Filters */}
{/* Search Toggle Button */}
{/* Status Filter */}
{/* Area Filter */}
{/* Sort Filter Button */}
{/* Collapsible Search Bar */}
setSearchQuery(e.target.value)} className="w-full bg-transparent border-none focus:ring-0 focus:outline-none dark:text-white" />
{/* Projects Grid/List */}
{displayProjects.length === 0 ? (
{t('projects.noProjectsFound')}
) : ( displayProjects.map((project) => ( getCompletionPercentage(project) } activeDropdown={activeDropdown} setActiveDropdown={setActiveDropdown} handleEditProject={handleEditProject} setProjectToDelete={setProjectToDelete} setIsConfirmDialogOpen={setIsConfirmDialogOpen} onOpenShare={(p) => setShareModal({ isOpen: true, project: p }) } /> )) )}
{modalState.isOpen && ( { setModalState({ isOpen: false, projectToEdit: null }); }} onSave={handleSaveProject} onDelete={async (projectUid) => { try { await deleteProject(projectUid); // Update both local and global state const updatedProjects = projects.filter( (p: Project) => p.uid !== projectUid ); setProjects(updatedProjects); setModalState({ isOpen: false, projectToEdit: null, }); } catch (error) { console.error('Error deleting project:', error); } }} project={modalState.projectToEdit || undefined} areas={areas} /> )} {isConfirmDialogOpen && ( setIsConfirmDialogOpen(false)} /> )} {shareModal.isOpen && shareModal.project && ( setShareModal({ isOpen: false, project: null }) } project={shareModal.project} /> )}
); }; export default Projects;