import React, { useState, useEffect, useRef, Fragment, useMemo, useCallback } from 'react'; import { useBlocker, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from '../hooks/useTranslation'; import { AlertCircle, UserPlus, Trash2, Shield, UploadCloud } from 'lucide-react'; import { Dialog, Transition } from '@headlessui/react'; import { toast } from 'sonner'; import { getPriceUnits, getWorkspaceUserRates, type PriceUnit, type WorkspaceUserRate } from '../api/rates'; import { updateWorkspace, addWorkspaceMembership, removeWorkspaceMembership, updateWorkspaceMembership, fetchWorkspaceMemberships, getWorkspace } from '../api/workspaces'; import { searchUserByExactMobile, type SearchedUser } from '../api/users'; import { useAppContext } from '../context/AppContext'; import { WORKSPACE_EDIT, WORKSPACE_MEMBERS_ADD, WORKSPACE_MEMBERS_CHANGE_ROLE, canChangeWorkspaceMember, canWorkspace, type WorkspaceRole, } from '../lib/permissions'; import { Button } from '../components/ui/button'; import { InfiniteScroll } from '../components/InfiniteScroll'; import { Select } from '../components/ui/Select'; import { Input } from '../components/ui/input'; import { TextAreaInput } from '../components/ui/TextAreaInput'; import WorkspaceMemberRateFields from '../components/rates/WorkspaceMemberRateFields'; const toEnglishDigits = (str: string) => { return str.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString()) .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d).toString()); }; const LIMIT = 10; export default function EditWorkspace() { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const { t, lang } = useTranslation(); const isFa = lang === 'fa'; const toPersianNum = (num: string | number | undefined | null) => { if (num === null || num === undefined) return num; if (!isFa) return num; return num.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d as any]); }; const { user } = useAppContext(); const currentUserId = user?.id || ''; // Workspace Info States const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [thumbnailUrl, setThumbnailUrl] = useState(null); const [thumbnailFile, setThumbnailFile] = useState(null); const [clearThumbnail, setClearThumbnail] = useState(false); const [thumbnailPreview, setThumbnailPreview] = useState(null); const [myRole, setMyRole] = useState('member'); const [workspaceOwnerId, setWorkspaceOwnerId] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [workspaceRates, setWorkspaceRates] = useState([]); const [priceUnits, setPriceUnits] = useState([]); // Members States const [members, setMembers] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [searchResult, setSearchResult] = useState(null); const [searchError, setSearchError] = useState(false); const [isSearching, setIsSearching] = useState(false); const [newMemberRole, setNewMemberRole] = useState<'owner' | 'admin' | 'member' | 'guest'>('member'); // Pagination States const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); const [isLoadingMembers, setIsLoadingMembers] = useState(false); // Modal State const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [memberIdToDelete, setMemberIdToDelete] = useState(null); const searchTimeoutRef = useRef | null>(null); const [initialData, setInitialData] = useState({ name: '', description: '', }); const hasUnsavedChanges = useMemo(() => { if (isLoading) return false; const isNameChanged = name.trim() !== (initialData.name || '').trim(); const isDescChanged = description.trim() !== (initialData.description || '').trim(); const isImageChanged = !!thumbnailFile || clearThumbnail; return isNameChanged || isDescChanged || isImageChanged; }, [name, description, initialData, isLoading, thumbnailFile, clearThumbnail]); useEffect(() => { if (!thumbnailFile) { setThumbnailPreview(null); return; } const objectUrl = URL.createObjectURL(thumbnailFile); setThumbnailPreview(objectUrl); return () => URL.revokeObjectURL(objectUrl); }, [thumbnailFile]); const handleThumbnailChange = (file: File | null) => { if (!file) { setThumbnailFile(null); return; } const allowedTypes = ["image/jpeg", "image/png", "image/webp"]; if (!allowedTypes.includes(file.type)) { toast.error(t.workspace?.thumbnailInvalidType || "Unsupported image type. Use JPG, PNG, or WebP."); return; } const maxBytes = 2 * 1024 * 1024; if (file.size > maxBytes) { toast.error(t.workspace?.thumbnailMaxSizeError || "Image size must be 2MB or less."); return; } setThumbnailFile(file); setClearThumbnail(false); }; useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (hasUnsavedChanges && !isSaving) { e.preventDefault(); e.returnValue = ''; } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, [hasUnsavedChanges, isSaving]); useBlocker(({ currentLocation, nextLocation }) => { if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) { return !window.confirm(t.confirmLeave || "تغییرات ذخیره نشده‌ای دارید. آیا مطمئن هستید که می‌خواهید خارج شوید؟"); } return false; }); useEffect(() => { if (id) loadData(); }, [id]); useEffect(() => { if (!isLoading && id && !canWorkspace(myRole, WORKSPACE_EDIT)) { toast.error("You do not have permission to edit this workspace."); navigate(`/workspaces/${id}`); } }, [id, isLoading, myRole, navigate]); const loadData = async () => { try { setIsLoading(true); const workspaceData = await getWorkspace(id!); setName(workspaceData.name); setDescription(workspaceData.description || ''); setThumbnailUrl(workspaceData.thumbnail || null); setThumbnailFile(null); setClearThumbnail(false); setMyRole(workspaceData.my_role || 'member'); setWorkspaceOwnerId(workspaceData.owner || ''); const [membersData, ratesData, unitsData] = await Promise.all([ fetchWorkspaceMemberships({ workspace: id!, limit: LIMIT, offset: 0 }), getWorkspaceUserRates(id!), getPriceUnits(), ]); const results = membersData.results || (Array.isArray(membersData) ? membersData : []); setMembers(results); setWorkspaceRates(ratesData.results || []); setPriceUnits(unitsData.results || []); setOffset(LIMIT); // Robust hasMore check: use `.next` if available, otherwise check if array filled the limit setHasMore(membersData.next ? true : results.length >= LIMIT); setInitialData({ name: workspaceData.name, description: workspaceData.description || '', }); } catch (error) { toast.error(t.workspace?.toast?.errorLoad || "Failed to load workspace data."); navigate('/workspaces'); } finally { setIsLoading(false); } }; const loadMoreMembers = useCallback(async () => { if (isLoadingMembers || !hasMore || !id) return; try { setIsLoadingMembers(true); // Send as pure numbers, axios handles them cleanly const membersData = await fetchWorkspaceMemberships({ workspace: id, limit: LIMIT, offset: offset }); const results = membersData.results || (Array.isArray(membersData) ? membersData : []); setMembers((prev) => { // Safe deduplication to avoid React key warnings const existingIds = new Set(prev.map(m => m.id)); const newItems = results.filter((item: any) => !existingIds.has(item.id)); return [...prev, ...newItems]; }); setOffset(offset + LIMIT); setHasMore(membersData.next ? true : results.length >= LIMIT); } catch (error) { console.error("Failed to load more members", error); } finally { setIsLoadingMembers(false); } }, [id, isLoadingMembers, hasMore, offset]); useEffect(() => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); const cleanQuery = toEnglishDigits(searchQuery.trim()); setSearchError(false); if (cleanQuery.length >= 10) { searchTimeoutRef.current = setTimeout(async () => { setIsSearching(true); try { const user = await searchUserByExactMobile(cleanQuery); if (user && user.id) { setSearchResult(user); 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]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || !id) return; try { setIsSaving(true); const updatedWorkspace = await updateWorkspace(id, { name, description, thumbnail: thumbnailFile, clear_thumbnail: clearThumbnail, }); toast.success(t.workspace?.toast?.successUpdate || "Workspace updated successfully."); window.dispatchEvent(new CustomEvent('workspace_edited', { detail: updatedWorkspace })); navigate('/workspaces'); } catch (error) { toast.error(t.workspace?.toast?.errorUpdate || "Failed to update workspace."); } finally { setIsSaving(false); } }; const handleAddMember = async () => { if (!searchResult || !id) return; try { const newMembership = await addWorkspaceMembership({ workspace: id, user: String(searchResult.id), role: newMemberRole }); setMembers([newMembership, ...members]); toast.success(t.workspace?.toast?.successAdd || "Member added successfully."); setSearchQuery(''); setSearchResult(null); setNewMemberRole('member'); } catch (error) { toast.error(t.workspace?.toast?.errorAdd || "Failed to add member."); } }; const openDeleteModal = (membershipId: string) => { setMemberIdToDelete(membershipId); setIsDeleteDialogOpen(true); }; const handleDeleteMember = async () => { if (!memberIdToDelete) return; try { await removeWorkspaceMembership(memberIdToDelete); setMembers(members.filter(m => m.id !== memberIdToDelete)); toast.success(t.workspace?.toast?.successRemove || "Member removed successfully."); setIsDeleteDialogOpen(false); } catch (error) { toast.error(t.workspace?.toast?.errorRemove || "Failed to remove member."); } }; const handleChangeRole = async (membershipId: string, newRole: string) => { try { await updateWorkspaceMembership(membershipId, { role: newRole }); setMembers(members.map(m => m.id === membershipId ? { ...m, role: newRole } : m)); toast.success(t.workspace?.toast?.successRole || "Role updated successfully."); } catch (error) { toast.error(t.workspace?.toast?.errorRole || "Failed to update role."); } }; const canManageMembers = canWorkspace(myRole, WORKSPACE_MEMBERS_CHANGE_ROLE); const isFirstOwner = currentUserId === workspaceOwnerId; const isOwner = myRole === "owner"; const roleOptions = (allowOwnerRole: boolean, allowAdminRole: boolean) => [ ...(allowOwnerRole ? [{ value: "owner", label: t.workspace?.roles?.owner || "Owner" }] : []), ...(allowAdminRole ? [{ value: "admin", label: t.workspace?.roles?.admin || "Admin" }] : []), { value: "member", label: t.workspace?.roles?.member || "Member" }, { value: "guest", label: t.workspace?.roles?.guest || "Guest" }, ]; if (isLoading) return
{t.workspace?.loading || "Loading..."}
; return (

{t.workspace?.editTitle || "Edit Workspace"}

setName(e.target.value)} className="w-full px-4 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500" required />
setDescription(e.target.value)} placeholder={t.workspace?.descriptionPlaceholder || "Optional description..."} className="w-full px-4 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 h-32 resize-none" />
{(thumbnailUrl || thumbnailFile) && ( )}

{ t.workspace?.members || "Members" }

{canWorkspace(myRole, WORKSPACE_MEMBERS_ADD) && (
setSearchQuery(toEnglishDigits(e.target.value))} className="w-full px-4 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500" dir="auto" /> {isSearching &&

{t.workspace?.searching || "Searching..."}

} {searchError && !isSearching && (
{t.workspace?.userNotFound || "No user found with this exact number."}
)} {searchResult && !isSearching && (
{searchResult.profile_picture ? ( {searchResult.first_name} ) : (
{searchResult.first_name?.[0] || "U"}
)}

{searchResult.first_name} {searchResult.last_name}

{toPersianNum(searchResult.mobile)}

handleChangeRole(m.id, val)} options={roleOptions(isFirstOwner, isOwner)} buttonClassName="w-[110px] px-3 py-1.5 text-sm" /> ) : ( {m.role === 'owner' && } {m.role && m.role in t.workspace.roles ? t.workspace.roles[m.role as keyof typeof t.workspace.roles] : m.role || "-"} )} {canChangeThisUserRole && ( )}
{t.rates?.workspaceRate || "Workspace rate"}
item.user === m.user.id)} priceUnits={priceUnits} onRatesChanged={(updater) => setWorkspaceRates((current) => updater(current))} />
); })} {members.length === 0 && !isLoadingMembers && (

{t.workspace?.noMembers || "No members found."}

)}
setIsDeleteDialogOpen(false)}>
{t.workspace?.confirmDeleteTitle || "Remove Member"}

{t.workspace?.confirmDeleteMessage || "Are you sure you want to remove this member from the workspace?"}

); }