import { useState, useEffect, useCallback, useRef } from "react"; import { useNavigate, useBlocker } from "react-router-dom"; import { Users, Briefcase, Trash2, Search, Loader2, } from "lucide-react"; import { toast } from "sonner"; import { createProject } from "../api/projects"; import { getClients } from "../api/clients"; import { fetchWorkspaceMemberships } from "../api/workspaces"; import { searchUserByExactMobile, type SearchedUser } from "../api/users"; import { useAppContext } from "../context/AppContext"; import { useWorkspace } from "../context/WorkspaceContext"; import { useTranslation } from "../hooks/useTranslation"; import { PROJECTS_CREATE, canWorkspace } from "../lib/permissions"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/Select"; import { TextAreaInput } from "../components/ui/TextAreaInput"; import { InfiniteScroll } from "../components/InfiniteScroll"; import { Modal } from "../components/Modal"; type ProjectRole = "manager" | "member"; interface LocalMember { localId: string; user: any; role: ProjectRole; isCreator?: boolean; } const COLORS = [ "#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#14B8A6", "#64748B", ]; const toEnglishDigits = (str: string) => { if (!str) return ""; return str .replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d).toString()) .replace(/[٠-٩]/g, (d) => "٠١٢٣٤٥٦٧٨٩".indexOf(d).toString()); }; const LIMIT = 10; export default function ProjectCreate() { const navigate = useNavigate(); const { t } = useTranslation(); const { user } = useAppContext(); const { activeWorkspace } = useWorkspace(); const currentUserId = user?.id || ""; const canCreateProject = canWorkspace(activeWorkspace?.my_role, PROJECTS_CREATE); // Project Detail States const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [color, setColor] = useState(COLORS[0]); const [client, setClient] = useState(""); const [clientsList, setClientsList] = useState([]); // Workspace List & Pagination States const [workspaceMembers, setWorkspaceMembers] = useState([]); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingData, setIsLoadingData] = useState(true); // Member Management States const [members, setMembers] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [addAllMembers, setAddAllMembers] = useState(false); const [isAddingAll, setIsAddingAll] = useState(false); // External Search States const [searchResult, setSearchResult] = useState(null); const [isSearching, setIsSearching] = useState(false); const [searchError, setSearchError] = useState(false); const searchTimeoutRef = useRef | null>(null); const [isSaving, setIsSaving] = useState(false); const [memberIdToDelete, setMemberIdToDelete] = useState(null); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const hasUnsavedChanges = name.trim() !== "" || description.trim() !== "" || members.length > 1; useEffect(() => { if (activeWorkspace && !canCreateProject) { toast.error("You do not have permission to create projects."); navigate("/projects"); } }, [activeWorkspace, canCreateProject, navigate]); useBlocker(({ currentLocation, nextLocation }) => { if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) { return !window.confirm(t.confirmLeave || "You have unsaved changes. Are you sure you want to leave?"); } return false; }); // EXACT same pagination structure as EditWorkspace.tsx useEffect(() => { if (activeWorkspace?.id) { const workspaceId = activeWorkspace.id; setName(""); setDescription(""); setColor(COLORS[0]); setClient(""); setClientsList([]); setWorkspaceMembers([]); setSearchQuery(""); setSearchResult(null); setSearchError(false); setAddAllMembers(false); // Reset pagination state setOffset(0); setHasMore(true); setIsLoadingData(true); if (user?.id) { setMembers([{ localId: user.id, user: user, role: "manager", isCreator: true }]); } else { setMembers([]); } const loadInitialData = async () => { try { const clientsRes = await getClients(workspaceId); setClientsList(clientsRes.results || []); const res = await fetchWorkspaceMemberships({ workspace: workspaceId, limit: LIMIT, offset: 0, }); const results = res.results || (Array.isArray(res) ? res : []); setWorkspaceMembers(results); setOffset(LIMIT); setHasMore(res.next ? true : results.length >= LIMIT); } catch (err) { console.error("Failed to fetch initial data", err); toast.error("Failed to load initial data."); } finally { setIsLoadingData(false); } }; loadInitialData(); } }, [activeWorkspace?.id, user?.id]); // EXACT same LoadMore logic and deduplication as EditWorkspace.tsx const loadMoreMembers = useCallback(async () => { if (isLoadingMore || !hasMore || !activeWorkspace?.id) return; try { setIsLoadingMore(true); const res = await fetchWorkspaceMemberships({ workspace: activeWorkspace.id, limit: LIMIT, offset: offset }); const results = res.results || (Array.isArray(res) ? res : []); setWorkspaceMembers((prev) => { // Safe deduplication to avoid React key warnings breaking the DOM observer const existingIds = new Set(prev.map(m => m.id)); const newItems = results.filter((item: any) => !existingIds.has(item.id)); return [...prev, ...newItems]; }); setOffset(prev => prev + LIMIT); setHasMore(res.next ? true : results.length >= LIMIT); } catch (error) { console.error("Failed to load more members", error); } finally { setIsLoadingMore(false); } }, [activeWorkspace?.id, isLoadingMore, hasMore, offset]); // Unified Search Logic useEffect(() => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); const cleanQuery = toEnglishDigits(searchQuery.trim()); setSearchError(false); if (cleanQuery.length >= 10 && /^\d+$/.test(cleanQuery)) { searchTimeoutRef.current = setTimeout(async () => { setIsSearching(true); try { const foundUser = await searchUserByExactMobile(cleanQuery); if (foundUser && foundUser.id) { if (foundUser.id === currentUserId) { setSearchResult(null); } else { setSearchResult(foundUser); setSearchError(false); } } else { setSearchResult(null); setSearchError(true); } } catch (error) { setSearchResult(null); setSearchError(true); } finally { setIsSearching(false); } }, 500); } else { setSearchResult(null); } return () => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); }; }, [searchQuery, currentUserId]); const handleAddMember = (userToAdd: any) => { if (members.some((m) => m.user.id === userToAdd.id)) return; const newMember: LocalMember = { localId: Math.random().toString(36).substr(2, 9), user: userToAdd, role: "member", }; setMembers((prev) => [newMember, ...prev]); setSearchQuery(""); setSearchResult(null); }; const handleToggleAddAllMembers = async () => { if (addAllMembers) { setMembers((prev) => prev.filter(m => m.isCreator || m.role === "manager")); setAddAllMembers(false); } else { if (!activeWorkspace?.id) return; setIsAddingAll(true); try { let currentOffset = 0; let continueFetching = true; const allWsMembers: any[] = []; while (continueFetching) { const res = await fetchWorkspaceMemberships({ workspace: activeWorkspace.id, limit: 50, offset: currentOffset, }); const fetchedResults = res.results || (Array.isArray(res) ? res : []); allWsMembers.push(...fetchedResults); if (res.next) { currentOffset += 50; } else { continueFetching = false; } } const newMembersToAdd = allWsMembers .map((wm) => wm.user) .filter((u) => u && u.id !== currentUserId && !members.some((m) => m.user.id === u.id)); const localMembers: LocalMember[] = newMembersToAdd.map((u) => ({ localId: Math.random().toString(36).substr(2, 9), user: u, role: "member", })); setMembers((prev) => [...prev, ...localMembers]); setAddAllMembers(true); } catch (error) { toast.error("Could not add all workspace members."); } finally { setIsAddingAll(false); } } }; const openDeleteModal = (userId: string) => { setMemberIdToDelete(userId); setIsDeleteDialogOpen(true); }; const handleDeleteMember = () => { if (!memberIdToDelete) return; setMembers(members.filter((m) => m.user.id !== memberIdToDelete)); setIsDeleteDialogOpen(false); setMemberIdToDelete(null); }; const handleChangeRole = (userId: string, newRole: string) => { setMembers( members.map((m) => (m.user.id === userId ? { ...m, role: newRole as ProjectRole } : m)) ); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || !activeWorkspace) return; try { setIsSaving(true); const membersPayload = members .filter((m) => !m.isCreator) .map((m) => ({ user_id: m.user.id, role: m.role })); const projectPayload: any = { name, description, color, workspace: activeWorkspace.id, members: membersPayload, }; if (client) projectPayload.client = client; const newProject = await createProject(projectPayload); window.dispatchEvent(new CustomEvent("project_created", { detail: newProject })); toast.success(t.projects?.createSuccess || "Project created successfully."); navigate("/projects"); } catch (error: any) { toast.error(error.message || t.projects?.createError || "Failed to create project."); } finally { setIsSaving(false); } }; // Prepare unified display list const filteredWorkspaceMembers = workspaceMembers.filter((m) => { const q = searchQuery.trim().toLowerCase(); if (!q) return true; const fullName = `${m.user.first_name || ""} ${m.user.last_name || ""}`.toLowerCase(); const phone = toEnglishDigits(m.user.mobile || ""); return fullName.includes(q) || phone.includes(toEnglishDigits(q)); }); const workspaceMemberUserIds = new Set(filteredWorkspaceMembers.map((m) => m.user.id)); const externalAddedMembers = members.filter((m) => { if (workspaceMemberUserIds.has(m.user.id)) return false; const q = searchQuery.trim().toLowerCase(); if (!q) return true; const fullName = `${m.user.first_name || ""} ${m.user.last_name || ""}`.toLowerCase(); const phone = toEnglishDigits(m.user.mobile || ""); return fullName.includes(q) || phone.includes(toEnglishDigits(q)); }); const displayList = [ ...externalAddedMembers.map((m) => ({ listId: m.localId, user: m.user })), ...filteredWorkspaceMembers.map((m) => ({ listId: m.id || m.user.id, user: m.user })) ]; if (!activeWorkspace) { return null; } return (

{t.projects?.createNew || "Create New Project"}

setName(e.target.value)} placeholder={t.projects?.namePlaceholder || "Project name..."} required />
{COLORS.map((c) => (
setSearchQuery(e.target.value)} placeholder={t.projects?.searchWorkspaceMembers || "Search by name or enter mobile number..."} className="ps-10" /> {isSearching && ( )}
{searchError && (

{t.projects?.userNotFound || "No user found with this mobile number."}

)} {searchResult && !searchError && (
{searchResult.profile_picture ? ( {searchResult.first_name} ) : (
{searchResult.first_name?.[0] || "U"}
)}
{searchResult.first_name} {searchResult.last_name} {searchResult.mobile}
)}
{isLoadingData ? (
{t.loading || "Loading..."}
) : ( {displayList.length === 0 ? (
{t.projects?.noWorkspaceMembers || "No members found."}
) : (
    {displayList.map((item) => { const addedMemberData = members.find((mm) => mm.user.id === item.user.id); const isAdded = !!addedMemberData; return (
  • {item.user.profile_picture ? ( {item.user.first_name} ) : (
    {item.user.first_name?.[0] || "U"}
    )}
    {item.user.first_name} {item.user.last_name} {addedMemberData?.isCreator && ( {t.projects?.creator || "Creator"} )} {item.user.mobile}
    {isAdded ? (
    {!addedMemberData.isCreator && (