"use client"; // ============================================================================ // Workspace Provider // ============================================================================ import React, { createContext, useContext, useState, useEffect, useCallback, } from "react"; import { useParams, useRouter, usePathname } from "next/navigation"; import { workspacesApi, projectsApi, membersApi, authApi } from "@/lib/api"; import type { Workspace, Project, Permission, PermissionKey, } from "@/lib/types/api"; import { STORAGE_KEYS } from "@/lib/utils/constants"; import { isDemoWorkspace, demoApi, demoWorkspace, demoProjects, DEMO_WORKSPACE_ID } from "@/lib/demo"; import { useAuth } from "@/components/auth/auth-provider"; // ============================================================================ // Types // ============================================================================ interface WorkspaceContextType { // Workspaces workspaces: Workspace[]; currentWorkspace: Workspace | null; setCurrentWorkspace: (workspace: Workspace) => void; // Projects (Target Channels) projects: Project[]; selectedProjects: Project[]; currentProject: Project | null; // First selected or null (for backward compatibility) setSelectedProjects: (projects: Project[]) => void; toggleProject: (project: Project) => void; selectAllProjects: () => void; clearProjectSelection: () => void; // Helper for API filtering // Returns project_id only if single project selected, undefined otherwise getProjectFilterId: () => string | undefined; // Returns true if all projects are selected allProjectsSelected: boolean; // Demo mode isDemoMode: boolean; // Permissions permissions: Permission[]; hasPermission: (key: PermissionKey, projectId?: string) => boolean; isAdmin: boolean; // Loading states isLoading: boolean; isLoadingProjects: boolean; error: string | null; // Actions refreshWorkspaces: () => Promise; refreshProjects: () => Promise; createWorkspace: (name: string) => Promise; } const WorkspaceContext = createContext( undefined ); // ============================================================================ // Hook // ============================================================================ export const useWorkspace = () => { const context = useContext(WorkspaceContext); if (!context) { throw new Error("useWorkspace must be used within WorkspaceProvider"); } return context; }; // ============================================================================ // Provider // ============================================================================ interface WorkspaceProviderProps { children: React.ReactNode; } export const WorkspaceProvider: React.FC = ({ children, }) => { const params = useParams(); const router = useRouter(); const pathname = usePathname(); const { user } = useAuth(); // State const [workspaces, setWorkspaces] = useState([]); const [currentWorkspace, setCurrentWorkspaceState] = useState(null); const [projects, setProjects] = useState([]); const [selectedProjects, setSelectedProjectsState] = useState([]); const [permissions, setPermissions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isLoadingProjects, setIsLoadingProjects] = useState(false); const [error, setError] = useState(null); // Get workspaceId from URL params const workspaceIdFromUrl = params?.workspaceId as string | undefined; // Check if demo mode const isDemoMode = isDemoWorkspace(workspaceIdFromUrl); // Current project (first selected for backward compatibility) const currentProject = selectedProjects.length > 0 ? selectedProjects[0] : null; // Check if all projects are selected const allProjectsSelected = projects.length > 0 && selectedProjects.length === projects.length; // Get project_id for API filtering // Returns undefined if multiple projects selected (to get workspace-wide data) const getProjectFilterId = useCallback(() => { if (selectedProjects.length === 1) { return selectedProjects[0].id; } // Multiple projects selected - return undefined to get all data return undefined; }, [selectedProjects]); // ============================================================================ // Permission Helpers // ============================================================================ const isAdmin = permissions.some((p) => p.key === "admin_full"); const hasPermission = useCallback( (key: PermissionKey, projectId?: string): boolean => { // Admin has all permissions if (isAdmin) return true; // Find permission by key const permission = permissions.find((p) => p.key === key); if (!permission) return false; // If no scopes defined, permission applies globally if (!permission.scopes || permission.scopes.length === 0) return true; // If projectId provided, check if it's in scopes if (projectId) { return permission.scopes.some( (s) => s.type === "project" && s.id === projectId ); } // Permission exists but is scoped - need projectId to verify return true; }, [permissions, isAdmin] ); // ============================================================================ // Load Workspaces // ============================================================================ const loadWorkspaces = useCallback(async () => { try { setIsLoading(true); setError(null); // Demo mode - use mock data, but also load real workspaces if user is logged in if (isDemoMode) { const allWorkspaces: Workspace[] = [demoWorkspace]; // If user is logged in, also load their real workspaces if (user) { try { const response = await workspacesApi.list({ size: 100 }); allWorkspaces.push(...response.items); } catch (err) { // If loading real workspaces fails, just use demo workspace console.warn("Failed to load real workspaces in demo mode:", err); } } setWorkspaces(allWorkspaces); setCurrentWorkspaceState(demoWorkspace); setIsLoading(false); return allWorkspaces; } const response = await workspacesApi.list({ size: 100 }); setWorkspaces(response.items); // If we have workspaceId in URL, select that workspace if (workspaceIdFromUrl && response.items.length > 0) { const workspace = response.items.find((w) => w.id === workspaceIdFromUrl); if (workspace) { setCurrentWorkspaceState(workspace); localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id); } else { // Workspace from URL not found, redirect to first available const firstWorkspace = response.items[0]; setCurrentWorkspaceState(firstWorkspace); localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, firstWorkspace.id); // Redirect to correct workspace const newPath = pathname.replace( `/dashboard/${workspaceIdFromUrl}`, `/dashboard/${firstWorkspace.id}` ); router.replace(newPath); } } else if (response.items.length > 0) { // Try to restore from localStorage or use first const savedId = localStorage.getItem(STORAGE_KEYS.SELECTED_WORKSPACE); const saved = savedId ? response.items.find((w) => w.id === savedId) : null; const workspace = saved || response.items[0]; setCurrentWorkspaceState(workspace); localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id); } return response.items; } catch (err: any) { console.error("Failed to load workspaces:", err); setError(err?.error?.message || "Ошибка загрузки воркспейсов"); return []; } finally { setIsLoading(false); } }, [workspaceIdFromUrl, pathname, router, isDemoMode, user]); // ============================================================================ // Load Projects & Permissions // ============================================================================ const loadProjectsAndPermissions = useCallback(async (workspaceId: string) => { try { setIsLoadingProjects(true); // Demo mode - use mock data if (isDemoMode) { const activeProjects = demoProjects.filter((p) => p.status === "active"); setProjects(activeProjects); setPermissions([{ key: "admin_full" }]); setSelectedProjectsState(activeProjects.length > 0 ? [activeProjects[0]] : []); setIsLoadingProjects(false); return; } // Load projects const projectsResponse = await projectsApi.list(workspaceId, { size: 100 }); const activeProjects = projectsResponse.items.filter( (p) => p.status === "active" ); setProjects(activeProjects); // Load current user's permissions const me = await authApi.me(); const membersResponse = await membersApi.list(workspaceId, { size: 100 }); // Find member by user.id (not by member record id) const currentMember = membersResponse.items.find((m) => m.user.id === me.id); if (currentMember) { // If member found but no permissions, they're likely the creator - grant admin if (currentMember.permissions.length === 0) { setPermissions([{ key: "admin_full" }]); } else { setPermissions(currentMember.permissions); } } else { // User not in members list - might be creator, check if only workspace // Grant admin access by default for workspace creators if (membersResponse.items.length === 0) { setPermissions([{ key: "admin_full" }]); } else { setPermissions([]); } } // Restore selected projects from localStorage const savedProjectIds = localStorage.getItem( `${STORAGE_KEYS.SELECTED_PROJECT}_${workspaceId}` ); if (savedProjectIds && activeProjects.length > 0) { try { const ids = JSON.parse(savedProjectIds) as string[]; const savedProjects = activeProjects.filter((p) => ids.includes(p.id)); if (savedProjects.length > 0) { setSelectedProjectsState(savedProjects); } else if (activeProjects.length > 0) { setSelectedProjectsState([activeProjects[0]]); } } catch { // Old format (single id), migrate to array const savedProject = activeProjects.find((p) => p.id === savedProjectIds); if (savedProject) { setSelectedProjectsState([savedProject]); } else if (activeProjects.length > 0) { setSelectedProjectsState([activeProjects[0]]); } } } else if (activeProjects.length > 0) { setSelectedProjectsState([activeProjects[0]]); } else { setSelectedProjectsState([]); } } catch (err: any) { console.error("Failed to load projects/permissions:", err); } finally { setIsLoadingProjects(false); } }, [isDemoMode]); // ============================================================================ // Set Current Workspace // ============================================================================ const setCurrentWorkspace = useCallback( (workspace: Workspace) => { setCurrentWorkspaceState(workspace); localStorage.setItem(STORAGE_KEYS.SELECTED_WORKSPACE, workspace.id); // Update URL if we're in dashboard if (pathname.startsWith("/dashboard/")) { const currentWorkspaceId = workspaceIdFromUrl; if (currentWorkspaceId && currentWorkspaceId !== workspace.id) { const newPath = pathname.replace( `/dashboard/${currentWorkspaceId}`, `/dashboard/${workspace.id}` ); router.push(newPath); } } else if (pathname === "/dashboard" || pathname === "/") { router.push(`/dashboard/${workspace.id}`); } // Load projects for new workspace loadProjectsAndPermissions(workspace.id); }, [pathname, router, workspaceIdFromUrl, loadProjectsAndPermissions] ); // ============================================================================ // Project Selection Methods // ============================================================================ const saveSelectedProjects = useCallback( (selected: Project[]) => { if (currentWorkspace) { localStorage.setItem( `${STORAGE_KEYS.SELECTED_PROJECT}_${currentWorkspace.id}`, JSON.stringify(selected.map((p) => p.id)) ); } }, [currentWorkspace] ); const setSelectedProjects = useCallback( (selected: Project[]) => { setSelectedProjectsState(selected); saveSelectedProjects(selected); }, [saveSelectedProjects] ); const toggleProject = useCallback( (project: Project) => { setSelectedProjectsState((prev) => { const isSelected = prev.some((p) => p.id === project.id); let newSelection: Project[]; if (isSelected) { // Don't allow deselecting if it's the only one if (prev.length <= 1) { return prev; } newSelection = prev.filter((p) => p.id !== project.id); } else { newSelection = [...prev, project]; } saveSelectedProjects(newSelection); return newSelection; }); }, [saveSelectedProjects] ); const selectAllProjects = useCallback(() => { setSelectedProjectsState(projects); saveSelectedProjects(projects); }, [projects, saveSelectedProjects]); const clearProjectSelection = useCallback(() => { if (projects.length > 0) { const firstProject = [projects[0]]; setSelectedProjectsState(firstProject); saveSelectedProjects(firstProject); } }, [projects, saveSelectedProjects]); // ============================================================================ // Create Workspace // ============================================================================ const createWorkspace = useCallback(async (name: string): Promise => { const workspace = await workspacesApi.create({ name }); setWorkspaces((prev) => [...prev, workspace]); return workspace; }, []); // ============================================================================ // Effects // ============================================================================ // Initial load useEffect(() => { loadWorkspaces(); }, [loadWorkspaces]); // Load projects when workspace changes useEffect(() => { if (currentWorkspace) { loadProjectsAndPermissions(currentWorkspace.id); } }, [currentWorkspace?.id, loadProjectsAndPermissions]); // ============================================================================ // Render // ============================================================================ return ( currentWorkspace ? loadProjectsAndPermissions(currentWorkspace.id) : Promise.resolve(), createWorkspace, }} > {children} ); };