"use client"; import { useState, useEffect, useMemo } from "react"; import { Loader2, Shield, Search, X, Plus, Check, Info } from "lucide-react"; import type { WorkspaceMember, Project, PermissionKey } from "@/lib/types/api"; import { PERMISSION_LABELS, PERMISSION_GROUPS, PERMISSION_ICONS } from "@/lib/types/permission"; import { hasGlobalPermission, hasScopedPermission, getScopesForPermission, setScopedPermission, toggleScopedPermission, removePermission, calculatePermissionDiff, } from "@/lib/types/permission"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; 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?: { type: string; id: string }[] }[]) => Promise; workspaceId: string; } interface PermissionState { key: PermissionKey; isGlobal: boolean; projectIds: string[]; } 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, }: 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 handleToggleGlobal = (key: PermissionKey) => { 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) => { 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 handleAddProject = (key: PermissionKey, projectId: string) => { setPermissions((prev) => { const existing = prev.find((p) => p.key === key); if (existing) { return prev.map((p) => p.key === key ? { ...p, isGlobal: false, projectIds: [...new Set([...p.projectIds, projectId])] } : p ); } return [ ...prev, { key, isGlobal: false, projectIds: [projectId] }, ]; }); }; const handleRemoveProject = (key: PermissionKey, projectId: string) => { setPermissions((prev) => { const existing = prev.find((p) => p.key === key); if (!existing) return prev; const newProjectIds = existing.projectIds.filter((id) => id !== projectId); if (newProjectIds.length === 0) { return prev.filter((p) => p.key !== key); } return prev.map((p) => p.key === key ? { ...p, projectIds: newProjectIds } : p ); }); }; 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 renderPermissionRow = (key: PermissionKey, group: string) => { const current = permissions.find((p) => p.key === key); const currentProjectIds = current?.projectIds || []; const isGlobal = current?.isGlobal ?? false; return (
{PERMISSION_ICONS[key]} {PERMISSION_LABELS[key]} {group}
setProjectSearch(e.target.value)} className="h-8" />
{availableProjects.map((project) => { const isSelected = currentProjectIds.includes(project.id); return (
handleToggleProject(key, project.id)} >
{project.title}
{project.username && (
@{project.username}
)}
{isSelected && }
); })} {availableProjects.length === 0 && (
Проекты не найдены
)}
{!isGlobal && currentProjectIds.length > 0 && (
{currentProjectIds.map((id) => { const project = getProjectById(id); return ( {project?.title || "Unknown"} handleRemoveProject(key, id)} /> ); })}
)}
); }; return ( Права доступа Настройте права доступа для участника {member.user.username || `User ${member.user.telegram_id}`}
Быстрые настройки: {PRESETS.map((preset) => ( ))}

👑 Администрирование

{PERMISSION_GROUPS.admin.map((key) => renderPermissionRow(key, "admin"))}

📁 Проекты

{PERMISSION_GROUPS.projects.map((key) => renderPermissionRow(key, "projects"))}

🎨 Креативы

{PERMISSION_GROUPS.creatives.map((key) => renderPermissionRow(key, "creatives"))}

📋 Закупы

{PERMISSION_GROUPS.placements.map((key) => renderPermissionRow(key, "placements"))}

📊 Аналитика

{PERMISSION_GROUPS.analytics.map((key) => renderPermissionRow(key, "analytics"))}
Глобальные права применяются ко всему воркспейсу. Права на проекты применяются только к выбранным проектам.
{isDirty && Есть несохранённые изменения}
); }