import React, { useState, useEffect } from "react"; import { useTranslation } from "../hooks/useTranslation"; import { getProjects, deleteProject, type Project } from "../api/projects"; import { useAppContext } from "../context/AppContext"; import { useWorkspace } from "../context/WorkspaceContext"; import { ProjectCreateModal } from "../components/projects/ProjectCreateModal"; import { ProjectEditModal } from "../components/projects/ProjectEditModal"; import { Pagination } from "../components/Pagination"; import { Plus, Archive, Trash2, Pencil } from "lucide-react"; import FilterBar from "../components/FilterBar"; import { Button } from "../components/ui/button"; import { Card } from "../components/ui/card"; import { Modal } from "../components/Modal"; import { toast } from "sonner"; import { Input } from "../components/ui/input"; import { PROJECTS_ARCHIVE, PROJECTS_CREATE, PROJECTS_EDIT, canDeleteWorkspaceResource, canWorkspace, } from "../lib/permissions"; export const Projects: React.FC = () => { const { t, lang } = useTranslation(); const { user } = useAppContext(); const { activeWorkspace } = useWorkspace(); const workspaceRole = activeWorkspace?.my_role; const canCreateProject = canWorkspace(workspaceRole, PROJECTS_CREATE); const canEditProject = canWorkspace(workspaceRole, PROJECTS_EDIT); const canArchiveProject = canWorkspace(workspaceRole, PROJECTS_ARCHIVE); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [editingProject, setEditingProject] = useState(null); const [search, setSearch] = useState(""); const [ordering, setOrdering] = useState("-created_at"); const [isArchived, setIsArchived] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(10); const [totalItems, setTotalItems] = useState(0); const [projectToDelete, setProjectToDelete] = useState(null); const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; project: Project | null}>({isOpen: false, project: null}); const [deleteInput, setDeleteInput] = useState(''); const orderingOptions = [ { value: '-created_at', label: t.ordering?.createdAtDesc || 'Newest First' }, { value: 'created_at', label: t.ordering?.createdAt || 'Oldest First' }, { value: 'name', label: t.ordering?.name || 'Name (A-Z)' }, { value: '-name', label: t.ordering?.nameDesc || 'Name (Z-A)' }, ]; const fetchProjectList = async () => { if (!activeWorkspace) return; setLoading(true); try { const offset = (currentPage - 1) * limit; const data = await getProjects(activeWorkspace.id, { limit, offset, search, is_archived: isArchived, ordering }); const items = data?.results || (Array.isArray(data) ? data : []) const count = data?.count !== undefined ? data.count : items.length setProjects(items); setTotalItems(count) } catch (error) { console.error("Failed to fetch projects", error); } finally { setLoading(false); } }; useEffect(() => { const delayDebounceFn = setTimeout(() => { fetchProjectList(); }, 300); return () => clearTimeout(delayDebounceFn); }, [activeWorkspace, currentPage, limit, search, isArchived, ordering]); useEffect(() => { const handleCreated = () => fetchProjectList(); const handleUpdated = () => fetchProjectList(); window.addEventListener("project_created", handleCreated); window.addEventListener("project_updated", handleUpdated); return () => { window.removeEventListener("project_created", handleCreated); window.removeEventListener("project_updated", handleUpdated); }; }, [activeWorkspace, currentPage, limit, search, isArchived, ordering]); const handleDeleteClick = (project: Project) => { setProjectToDelete(project); }; const confirmDelete = async () => { if (!deleteModal.project) return; try { const deletedId = deleteModal.project.id; await deleteProject(deletedId); fetchProjectList(); window.dispatchEvent(new CustomEvent('project_deleted', { detail: { id: deletedId } })); toast.success(t.projects?.deleteSuccess || 'Project deleted successfully'); setDeleteModal({ isOpen: false, project: null }); setDeleteInput(''); } catch (error) { toast.error(t.projects?.deleteError || 'Failed to delete project'); } }; const formatDate = (dateStr: string | undefined) => { if (!dateStr) return "-" try { const date = new Date(dateStr) return new Intl.DateTimeFormat(lang === "fa" ? "fa-IR" : "en-US", { dateStyle: "long", timeZone: "Asia/Tehran", }).format(date) } catch { return dateStr } } return (

{t.projects?.title || 'Projects'}

{t.projects?.description(activeWorkspace?.name || "-") || 'Manage your projects'}

{canArchiveProject && ( )} {canCreateProject && ( )}
{loading ? (
{t.projects?.loading || 'Loading...'}
) : (
{projects.length === 0 ? (

{t.projects?.emptyState || 'No projects found'}

) : (
    {projects.map((project) => { const canDeleteProject = canDeleteWorkspaceResource({ workspaceRole, currentUserId: user?.id, createdById: project.created_by?.id, }); return (
  • {project.name}

    {project.client ? `${t.projects?.client || "Client"}: ${project.client.name}` : t.projects?.noClient || "No client"}

    {project.description && (

    {project.description}

    )}
    {(canEditProject || canDeleteProject) && (
    {canEditProject && ( )} {canDeleteProject && ( )}
    )}
  • ); })}
)}
)} {/* Modals */} {canCreateProject && isCreateModalOpen && ( setIsCreateModalOpen(false)} /> )} {canEditProject && editingProject && ( setEditingProject(null)} /> )} {deleteModal.project && ( { setDeleteModal({ isOpen: false, project: null }); setDeleteInput(''); }} title={t.projects?.deleteTitle || 'Delete Project'} maxWidth="max-w-md" footer={ <> } >

{t.projects?.deleteWarning || 'To confirm deletion, please type the project name:'} {deleteModal.project.name}

setDeleteInput(e.target.value)} placeholder={deleteModal.project.name} />
)}
); };