"use client"; import { useState, useEffect, useMemo } from "react"; import { Loader2, Shield, Search, X, Check, Info, Crown, FolderOpen, Palette, ClipboardList, BarChart3, Users, Lock, Eye, Pencil, ChartArea, UserRoundX, FileUser, Globe, } from "lucide-react"; import type { WorkspaceMember, Project, PermissionKey, PermissionScope } from "@/lib/types/api"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; interface PermissionModalProps { open: boolean; onOpenChange: (open: boolean) => void; member: WorkspaceMember; projects: Project[]; onSave: (workspaceId: string, memberId: string, userId: string, permissions: { key: PermissionKey; scopes?: PermissionScope[] }[]) => Promise; workspaceId: string; currentUserId: string; isOnlyAdmin: boolean; } interface PermissionState { key: PermissionKey; isGlobal: boolean; projectIds: string[]; } const PERMISSION_CONFIG: Record = { admin_full: { icon: Crown, label: "Полный доступ", group: "admin" }, projects_read: { icon: Eye, label: "Просмотр проектов", group: "projects" }, projects_write: { icon: Pencil, label: "Управление проектами", group: "projects" }, creatives_read: { icon: Eye, label: "Просмотр креативов", group: "creatives" }, creatives_write: { icon: Pencil, label: "Создание и редактирование креативов", group: "creatives" }, placements_read: { icon: Eye, label: "Просмотр закупок", group: "placements" }, placements_write: { icon: Pencil, label: "Создание и редактирование закупок", group: "placements" }, analytics_read: { icon: ChartArea, label: "Полный доступ к статистике", group: "analytics" }, analytics_without_clicks: { icon: UserRoundX, label: "Статистика без переходов", group: "analytics" }, analytics_own_creatives: { icon: FileUser, label: "Статистика только своих креативов", group: "analytics" }, }; const GROUPS = [ { key: "admin", label: "Администрирование", icon: Shield }, { key: "projects", label: "Проекты", icon: FolderOpen }, { key: "creatives", label: "Креативы", icon: Palette }, { key: "placements", label: "Закупки", icon: ClipboardList }, { key: "analytics", label: "Аналитика", icon: BarChart3 }, ] as const; const PRESETS = [ { label: "Полный доступ", keys: ["admin_full"] }, { label: "Только чтение", keys: ["projects_read", "creatives_read", "placements_read", "analytics_read"] }, { label: "Менеджер", keys: ["projects_read", "projects_write", "placements_read", "placements_write"] }, { label: "Убрать все", keys: [] }, ]; export function PermissionModal({ open, onOpenChange, member, projects, onSave, workspaceId, currentUserId, isOnlyAdmin, }: PermissionModalProps) { const [isSaving, setIsSaving] = useState(false); const [permissions, setPermissions] = useState([]); const [projectSearch, setProjectSearch] = useState(""); useEffect(() => { if (open && member) { const perms: PermissionState[] = []; for (const perm of member.permissions) { const isGlobal = !perm.scopes || perm.scopes.length === 0; const projectIds = perm.scopes ? perm.scopes.filter((s) => s.type === "project").map((s) => s.id) : []; perms.push({ key: perm.key, isGlobal, projectIds, }); } setPermissions(perms); } }, [open, member]); const hasGlobalAdmin = useMemo(() => { const admin = permissions.find((p) => p.key === "admin_full"); return admin?.isGlobal ?? false; }, [permissions]); const isPermissionDisabled = (key: PermissionKey): boolean => { return hasGlobalAdmin && key !== "admin_full"; }; const handleToggleGlobal = (key: PermissionKey) => { if (hasGlobalAdmin && key !== "admin_full") return; setPermissions((prev) => { const existing = prev.find((p) => p.key === key); if (existing) { return prev.map((p) => p.key === key ? { ...p, isGlobal: !p.isGlobal, projectIds: [] } : p ); } return [...prev, { key, isGlobal: true, projectIds: [] }]; }); }; const handleToggleProject = (key: PermissionKey, projectId: string) => { if (hasGlobalAdmin && key !== "admin_full") return; setPermissions((prev) => { const existing = prev.find((p) => p.key === key); if (existing) { return prev.map((p) => p.key === key ? { ...p, isGlobal: false, projectIds: toggleInArray(p.projectIds, projectId) } : p ); } return [ ...prev, { key, isGlobal: false, projectIds: [projectId] }, ]; }); }; const toggleInArray = (arr: string[], item: string): string[] => { if (arr.includes(item)) { return arr.filter((i) => i !== item); } return [...arr, item]; }; const applyPreset = (keys: PermissionKey[]) => { if (keys.length === 0) { setPermissions([]); return; } setPermissions( keys.map((key) => ({ key, isGlobal: true, projectIds: [], })) ); }; const getProjectById = (id: string): Project | undefined => { return projects.find((p) => p.id === id); }; const availableProjects = useMemo(() => { return projects.filter( (p) => p.title.toLowerCase().includes(projectSearch.toLowerCase()) || p.username?.toLowerCase().includes(projectSearch.toLowerCase()) ); }, [projects, projectSearch]); const isDirty = useMemo(() => { if (permissions.length !== member.permissions.length) return true; for (const perm of permissions) { const orig = member.permissions.find((p) => p.key === perm.key); if (!orig) return true; const origIsGlobal = !orig.scopes || orig.scopes.length === 0; const origProjectIds = orig.scopes ? orig.scopes.filter((s) => s.type === "project").map((s) => s.id) : []; if (perm.isGlobal !== origIsGlobal) return true; if (!perm.isGlobal) { const permProjectSet = new Set(perm.projectIds); const origProjectSet = new Set(origProjectIds); if (permProjectSet.size !== origProjectSet.size) return true; for (const id of permProjectSet) { if (!origProjectSet.has(id)) return true; } } } return false; }, [permissions, member.permissions]); const handleSave = async () => { setIsSaving(true); try { const result = permissions .filter((p) => { if (p.isGlobal) return true; return p.projectIds.length > 0; }) .map((p) => { if (p.isGlobal) { return { key: p.key }; } return { key: p.key, scopes: p.projectIds.map((id) => ({ type: "project" as const, id })), }; }); await onSave(workspaceId, member.id, member.user.id, result); onOpenChange(false); } finally { setIsSaving(false); } }; const handleReset = () => { const perms: PermissionState[] = []; for (const perm of member.permissions) { const isGlobal = !perm.scopes || perm.scopes.length === 0; const projectIds = perm.scopes ? perm.scopes.filter((s) => s.type === "project").map((s) => s.id) : []; perms.push({ key: perm.key, isGlobal, projectIds, }); } setPermissions(perms); }; const isEditingOwnPermissions = member.user.id === currentUserId; const renderPermissionRow = (key: PermissionKey) => { const config = PERMISSION_CONFIG[key]; const current = permissions.find((p) => p.key === key); const currentProjectIds = current?.projectIds || []; const isGlobal = current?.isGlobal ?? false; const isDisabled = isPermissionDisabled(key); return (
{isDisabled ? ( ) : ( )} {config.label} {isDisabled && ( Выдан полный доступ )}
setProjectSearch(e.target.value)} className="h-8" />
{availableProjects.map((project) => { const isSelected = currentProjectIds.includes(project.id); return (
handleToggleProject(key, project.id)} > {project.title.charAt(0).toUpperCase()}
{project.title}
{project.username && (
@{project.username}
)}
{isSelected && }
); })} {availableProjects.length === 0 && (
Проекты не найдены
)}
{!isGlobal && currentProjectIds.length > 0 && (
{currentProjectIds.map((id) => { const project = getProjectById(id); return ( {project?.title.charAt(0).toUpperCase() || "?"} {project?.title || "Unknown"} { e.stopPropagation(); if (isDisabled) return; setPermissions((prev) => { const existing = prev.find((p) => p.key === key); if (!existing) return prev; const newProjectIds = existing.projectIds.filter((pid) => pid !== id); if (newProjectIds.length === 0) { return prev.filter((p) => p.key !== key); } return prev.map((p) => p.key === key ? { ...p, projectIds: newProjectIds } : p ); }); }} > ); })}
)}
); }; return ( Права доступа Настройте права доступа для участника{" "} {member.user.first_name || member.user.username || `User ${member.user.telegram_id}`} {isEditingOwnPermissions && isOnlyAdmin && (
Вы не можете изменить свои права, так как вы единственный администратор воркспейса.
)}
Быстрые настройки: {PRESETS.map((preset) => ( ))}
{GROUPS.slice(1).map((group) => { const groupPermissions = Object.entries(PERMISSION_CONFIG) .filter(([, config]) => config.group === group.key) .map(([key, config]) => ({ ...config, key: key as PermissionKey })); if (groupPermissions.length === 0) return null; return (
{group.label}
{groupPermissions.map((perm) => renderPermissionRow(perm.key))}
); })}
Администрирование
{Object.entries(PERMISSION_CONFIG) .filter(([, config]) => config.group === "admin") .map(([key, config]) => ({ ...config, key: key as PermissionKey })) .map((perm) => renderPermissionRow(perm.key))}
Полный доступ автоматически включает все остальные права. Остальные права можно оставить для случая, если позже потребуется убрать полный доступ.
{isDirty && Есть несохранённые изменения}
); }