import React, { useState, useEffect, useMemo } 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'; const Projects: React.FC = () => { const { t } = useTranslation(); 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); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [projectToEdit, setProjectToEdit] = useState(null); const [projectToDelete, setProjectToDelete] = useState( null ); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [activeDropdown, setActiveDropdown] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [viewMode, setViewMode] = useState<'cards' | 'list'>('cards'); const [isSearchExpanded, setIsSearchExpanded] = useState(false); const [orderBy, setOrderBy] = useState('created_at:desc'); const [searchParams, setSearchParams] = useSearchParams(); const activeFilter = searchParams.get('active') || 'all'; const areaFilter = searchParams.get('area_id') || ''; // 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: 'true', label: t('projects.filters.active') }, { value: 'false', label: t('projects.filters.inactive') }, ]; const areaOptions: FilterOption[] = [ { value: '', label: t('projects.filters.allAreas') }, ...areas.map((area) => ({ value: area.id?.toString() || '', 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(); }, []); useEffect(() => { const loadProjects = async () => { try { const projectsData = await fetchProjects(); setProjects(projectsData); } catch (error) { console.error('Failed to fetch projects:', error); setProjectsError(true); } }; loadProjects(); }, []); // Handle click outside to close dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; // Check if the click is on a dropdown or its children const dropdownElement = target.closest('.dropdown-container'); if (!dropdownElement && activeDropdown !== null) { setActiveDropdown(null); } }; if (activeDropdown !== null) { // Use setTimeout to avoid immediate triggering setTimeout(() => { document.addEventListener('mousedown', handleClickOutside); }, 100); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [activeDropdown]); // Handle sort change const handleSortChange = (newOrderBy: string) => { setOrderBy(newOrderBy); }; const handleSaveProject = async (project: Project) => { setProjectsLoading(true); try { if (project.id) { await updateProject(project.id, project); } else { await createProject(project); } const projectsData = await fetchProjects(); setProjects(projectsData); } catch (error) { console.error('Error saving project:', error); setProjectsError(true); } finally { setProjectsLoading(false); setIsProjectModalOpen(false); } }; const handleEditProject = (project: Project) => { setProjectToEdit(project); setIsProjectModalOpen(true); }; const handleDeleteProject = async () => { if (!projectToDelete) return; try { if (projectToDelete.id !== undefined) { setProjectsLoading(true); await deleteProject(projectToDelete.id); // Update global state const projectsData = await fetchProjects(); setProjects(projectsData); } else { console.error('Cannot delete project: ID is undefined.'); } } catch (error) { console.error('Error deleting project:', error); setProjectsError(true); } 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 handleActiveFilterChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value === 'all') { params.delete('active'); } else { params.set('active', value); } setSearchParams(params); }; const handleAreaFilterChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value === '') { params.delete('area_id'); } else { params.set('area_id', value); } setSearchParams(params); }; // Filter, sort and search projects const displayProjects = useMemo(() => { let filteredProjects = [...projects]; // Apply active filter if (activeFilter !== 'all') { const isActive = activeFilter === 'true'; filteredProjects = filteredProjects.filter( (project) => project.active === isActive ); } // Apply area filter if (areaFilter) { const areaId = parseInt(areaFilter); filteredProjects = filteredProjects.filter( (project) => project.area_id === areaId ); } // 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, activeFilter, areaFilter, 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} /> )) )}
{isProjectModalOpen && ( { setIsProjectModalOpen(false); setProjectToEdit(null); }} onSave={handleSaveProject} onDelete={async (projectId) => { try { await deleteProject(projectId); // Update both local and global state const updatedProjects = projects.filter( (p: Project) => p.id !== projectId ); setProjects(updatedProjects); setIsProjectModalOpen(false); setProjectToEdit(null); } catch (error) { console.error('Error deleting project:', error); } }} project={projectToEdit || undefined} areas={areas} /> )} {isConfirmDialogOpen && ( setIsConfirmDialogOpen(false)} /> )}
); }; export default Projects;