"use client"; // ============================================================================ // Workspace Settings Page // ============================================================================ import { useState, useEffect, useRef, useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; import { Loader2, Settings, Pencil, Trash2, Users, Shield, ChevronDown, UserPlus, Mail, Clock, Upload, ImageIcon } from "lucide-react"; import { toast } from "sonner"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { useWorkspace } from "@/components/providers/workspace-provider"; import { useAuth } from "@/components/auth/auth-provider"; import { workspacesApi, membersApi, invitesApi } from "@/lib/api"; import type { User } from "@/lib/types/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { PERMISSION_LABELS } from "@/lib/types/api"; import type { WorkspaceMember, WorkspaceInvite, Project, PermissionKey, PermissionScope } from "@/lib/types/api"; import { PermissionModal } from "@/components/ui/permission-modal"; import { handleApiError, showPermissionErrorToast, showSuccessToast, } from "@/lib/utils/error-handler"; // ============================================================================ // Component // ============================================================================ export default function SettingsPage() { const params = useParams(); const router = useRouter(); const workspaceId = params?.workspaceId as string; const { currentWorkspace, refreshWorkspaces } = useWorkspace(); const { user } = useAuth(); const currentUser = user; const [name, setName] = useState(currentWorkspace?.name || ""); const [isUpdating, setIsUpdating] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(""); const [showDeleteDialog, setShowDeleteDialog] = useState(false); // Avatar upload const [avatarFile, setAvatarFile] = useState(null); const [isUploadingAvatar, setIsUploadingAvatar] = useState(false); const [isDeletingAvatar, setIsDeletingAvatar] = useState(false); const fileInputRef = useRef(null); // Members and invites const [members, setMembers] = useState([]); const [invites, setInvites] = useState([]); const [loadingMembers, setLoadingMembers] = useState(true); const [updating, setUpdating] = useState(null); const [showInviteDialog, setShowInviteDialog] = useState(false); const [username, setUsername] = useState(""); const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); // Permission modal const [showPermissionModal, setShowPermissionModal] = useState(false); const [selectedMember, setSelectedMember] = useState(null); const [projects, setProjects] = useState([]); const [loadingProjects, setLoadingProjects] = useState(false); const isOnlyAdmin = useMemo(() => { if (!currentUser || !members.length) return false; const adminMembers = members.filter((m) => m.permissions.some((p) => p.key === "admin_full") ); return adminMembers.length === 1 && adminMembers[0]?.user.id === currentUser.id; }, [currentUser, members]); useEffect(() => { if (currentWorkspace) { setName(currentWorkspace.name); } }, [currentWorkspace]); useEffect(() => { loadMembers(); }, [workspaceId]); const loadMembers = async () => { try { setLoadingMembers(true); const [membersResponse, invitesResponse] = await Promise.all([ membersApi.list(workspaceId, { size: 100 }), invitesApi.list(workspaceId, { size: 100 }), ]); setMembers(membersResponse.items); setInvites(invitesResponse.items); } catch (err) { console.error("Failed to load data:", err); } finally { setLoadingMembers(false); } }; const loadProjects = async () => { try { setLoadingProjects(true); const response = await workspacesApi.getProjects(workspaceId, { size: 100 }); setProjects(response.items); } catch (err) { console.error("Failed to load projects:", err); } finally { setLoadingProjects(false); } }; const openPermissionModal = async (member: WorkspaceMember) => { setSelectedMember(member); await loadProjects(); setShowPermissionModal(true); }; const handlePermissionSave = async ( wsId: string, memberId: string, userId: string, permissions: { key: PermissionKey; scopes?: PermissionScope[] }[] ) => { await toast.promise( membersApi.updatePermissions(wsId, memberId, { permissions }), { loading: "Сохранение прав доступа...", success: () => { loadMembers(); return "Права доступа обновлены"; }, error: (err) => { handleApiError(err); return "Не удалось сохранить права"; }, } ); setShowPermissionModal(false); }; const handleUpdate = async () => { if (!name.trim() || name === currentWorkspace?.name) return; await toast.promise( workspacesApi.update(workspaceId, { name: name.trim() }), { loading: "Сохранение названия...", success: () => { refreshWorkspaces(); return "Название обновлено"; }, error: (err) => { handleApiError(err); return "Не удалось сохранить название"; }, } ); setIsUpdating(false); }; const handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; await toast.promise( workspacesApi.uploadAvatar(workspaceId, file), { loading: "Загрузка аватара...", success: () => { refreshWorkspaces(); if (fileInputRef.current) { fileInputRef.current.value = ""; } setAvatarFile(null); return "Аватар загружен"; }, error: (err) => { handleApiError(err); return "Не удалось загрузить аватар"; }, } ); setIsUploadingAvatar(false); }; const handleAvatarDelete = async () => { await toast.promise( workspacesApi.deleteAvatar(workspaceId), { loading: "Удаление аватара...", success: () => { refreshWorkspaces(); return "Аватар удалён"; }, error: (err) => { handleApiError(err); return "Не удалось удалить аватар"; }, } ); setIsDeletingAvatar(false); }; const handleCreateInvite = async () => { if (!username.trim()) return; const cleanUsername = username.trim().replace(/^@/, ""); await toast.promise( invitesApi.create(workspaceId, { username: cleanUsername, }), { loading: "Отправка приглашения...", success: (invite) => { setInvites((prev) => [invite, ...prev]); setShowInviteDialog(false); setUsername(""); return `Приглашение отправлено @${cleanUsername}`; }, error: (err: any) => { const errorMessage = err?.error?.message || err?.detail; if (errorMessage?.includes("not found")) { setError(`Пользователь @${cleanUsername} не зарегистрирован на платформе.`); return "Пользователь не найден"; } if (errorMessage?.includes("already exists") || errorMessage?.includes("уже")) { return "Приглашение уже существует или пользователь уже участник"; } handleApiError(err); return "Не удалось отправить приглашение"; }, } ); setIsCreating(false); }; const handleDelete = async () => { if (deleteConfirm !== `Удалить ${currentWorkspace?.name}`) return; await toast.promise( workspacesApi.delete(workspaceId), { loading: "Удаление воркспейса...", success: () => { router.replace("/"); return "Воркспейс удалён"; }, error: (err) => { handleApiError(err); return "Не удалось удалить воркспейс"; }, } ); setIsDeleting(false); setShowDeleteDialog(false); setDeleteConfirm(""); }; const hasPermission = (member: WorkspaceMember, key: PermissionKey) => { return member.permissions.some((p) => p.key === key); }; const renderPermissionBadges = (member: WorkspaceMember) => { if (member.permissions.length === 0) { return Нет прав; } const adminPerm = member.permissions.find((p) => p.key === "admin_full"); if (adminPerm) { const isGlobal = !adminPerm.scopes || adminPerm.scopes.length === 0; return (
Админ {isGlobal ? "" : "(ограничен)"}
); } return (
{member.permissions.map((p) => { const isGlobal = !p.scopes || p.scopes.length === 0; const projectCount = p.scopes ? p.scopes.filter((s) => s.type === "project").length : 0; return ( {PERMISSION_LABELS[p.key]} {!isGlobal && projectCount > 0 && ( ({projectCount}) )} ); })}
); }; // Combine members and pending invites const pendingInvites = invites.filter((inv) => inv.status === "pending"); const allPeople = [ ...members.map((m) => ({ type: "member" as const, data: m })), ...pendingInvites.map((inv) => ({ type: "invite" as const, data: inv })), ]; if (!currentWorkspace) { return ( <>
); } return ( <>

Настройки воркспейса

Управление настройками воркспейса

{/* General Settings */} Основные настройки Название и аватар воркспейса {/* Avatar */}
{currentWorkspace.name.charAt(0).toUpperCase()}
{currentWorkspace.avatar_url && ( Удалить аватар Вы уверены, что хотите удалить аватар воркспейса? Отмена Удалить )}

PNG, JPG или GIF. Максимум 2 МБ.

{/* Name */}
setName(e.target.value)} placeholder="Название воркспейса" />
{/* Team Management */}
Участники Управление участниками воркспейса
Пригласить участника Введите username Telegram пользователя для отправки приглашения через бота
setUsername(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleCreateInvite(); } }} />
{error && (
{error}
)}
{loadingMembers ? (
) : allPeople.length === 0 ? (

Нет участников

Пригласите участников в воркспейс

) : ( Пользователь Статус Права доступа {allPeople.map((item) => { if (item.type === "invite") { const invite = item.data; return (
@{invite.username}
Ожидает принятия
); } else { const member = item.data; return (
{[member.user.first_name, member.user.last_name] .filter(Boolean) .join(" ") || member.user.username || `User ${member.user.telegram_id}`}
{member.user.username && (
@{member.user.username}
)}
{member.status === "active" ? "Активен" : "Приглашён"} {renderPermissionBadges(member)}
); } })}
)}
{/* Danger Zone */} Опасная зона Необратимые действия с воркспейсом Удалить воркспейс? Это действие необратимо. Все данные воркспейса будут удалены, включая проекты, креативы, размещения и аналитику.
setDeleteConfirm(e.target.value)} placeholder={`Удалить ${currentWorkspace.name}`} className="font-mono" />
{ setShowDeleteDialog(false); setDeleteConfirm(""); }}> Отмена {isDeleting ? ( <> Удаление... ) : ( "Удалить" )}
{selectedMember && ( )}
); }