import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { useAuth } from './AuthContext'; import { useUser } from './UserContext'; import { logger } from '../debugConfig'; import { CCUserNodeProps, CCCalendarNodeProps, CCUserTeacherTimetableNodeProps } from '../utils/tldraw/cc-base/cc-graph/cc-graph-types'; import { CalendarStructure, WorkerStructure } from '../types/navigation'; import { useNavigationStore } from '../stores/navigationStore'; // Core Node Types export interface CalendarNode { id: string; label: string; title: string; node_storage_path: string; type?: CCCalendarNodeProps['__primarylabel__']; nodeData?: CCCalendarNodeProps; } export interface WorkerNode { id: string; label: string; title: string; node_storage_path: string; type?: CCUserTeacherTimetableNodeProps['__primarylabel__']; nodeData?: CCUserTeacherTimetableNodeProps; } // Calendar Structure Types export interface CalendarDay { id: string; date: string; title: string; } export interface CalendarWeek { id: string; title: string; days: { id: string }[]; startDate: string; endDate: string; } export interface CalendarMonth { id: string; title: string; days: { id: string }[]; weeks: { id: string }[]; year: string; month: string; } export interface CalendarYear { id: string; title: string; months: { id: string }[]; year: string; } // Worker Structure Types export interface TimetableEntry { id: string; title: string; type: string; startTime: string; endTime: string; } export interface ClassEntry { id: string; title: string; type: string; } export interface LessonEntry { id: string; title: string; type: string; } interface NeoUserContextType { userNode: CCUserNodeProps | null; calendarNode: CalendarNode | null; workerNode: WorkerNode | null; userDbName: string | null; workerDbName: string | null; isLoading: boolean; isInitialized: boolean; error: string | null; // Calendar Navigation navigateToDay: (id: string) => Promise; navigateToWeek: (id: string) => Promise; navigateToMonth: (id: string) => Promise; navigateToYear: (id: string) => Promise; currentCalendarNode: CalendarNode | null; calendarStructure: CalendarStructure | null; // Worker Navigation navigateToTimetable: (id: string) => Promise; navigateToJournal: (id: string) => Promise; navigateToPlanner: (id: string) => Promise; navigateToClass: (id: string) => Promise; navigateToLesson: (id: string) => Promise; currentWorkerNode: WorkerNode | null; workerStructure: WorkerStructure | null; } const NeoUserContext = createContext({ userNode: null, calendarNode: null, workerNode: null, userDbName: null, workerDbName: null, isLoading: false, isInitialized: false, error: null, navigateToDay: async () => {}, navigateToWeek: async () => {}, navigateToMonth: async () => {}, navigateToYear: async () => {}, navigateToTimetable: async () => {}, navigateToJournal: async () => {}, navigateToPlanner: async () => {}, navigateToClass: async () => {}, navigateToLesson: async () => {}, currentCalendarNode: null, currentWorkerNode: null, calendarStructure: null, workerStructure: null }); export const NeoUserProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const { user, accessToken } = useAuth(); const { profile, isInitialized: isUserInitialized } = useUser(); const navigationStore = useNavigationStore(); const [userNode, setUserNode] = useState(null); const [calendarNode] = useState(null); const [workerNode] = useState(null); const [currentCalendarNode, setCurrentCalendarNode] = useState(null); const [currentWorkerNode, setCurrentWorkerNode] = useState(null); const [calendarStructure] = useState(null); const [workerStructure] = useState(null); const [userDbName, setUserDbName] = useState(null); const [workerDbName, setWorkerDbName] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isInitialized, setIsInitialized] = useState(false); const [error, setError] = useState(null); // Use ref for initialization tracking to prevent re-renders const initializationRef = React.useRef({ hasStarted: false, isComplete: false }); // Add base properties for node data const getBaseNodeProps = () => ({ title: '', w: 200, h: 200, headerColor: '#000000', backgroundColor: '#ffffff', isLocked: false, __primarylabel__: 'UserTeacherTimetable', uuid_string: '', node_storage_path: '', created: new Date().toISOString(), merged: new Date().toISOString(), state: { parentId: null, isPageChild: false, hasChildren: false, bindings: [], }, defaultComponent: true, }); // Initialize context when dependencies are ready useEffect(() => { logger.debug('neo-user-context', '🔄 useEffect triggered', { isUserInitialized, hasProfile: !!profile, hasUser: !!user, isInitialized, hasStarted: initializationRef.current.hasStarted }); if (!isUserInitialized) { logger.debug('neo-user-context', 'âŗ Waiting for user context initialization'); return; } if (!profile) { if (!initializationRef.current.isComplete) { setIsLoading(false); setIsInitialized(true); initializationRef.current.isComplete = true; logger.debug('neo-user-context', 'â„šī¸ No profile available; marking context initialized'); } return; } if (isInitialized || initializationRef.current.hasStarted) { logger.debug('neo-user-context', 'â„šī¸ Initialization already in progress or complete', { isInitialized, hasStarted: initializationRef.current.hasStarted }); return; } const initializeContext = async () => { try { initializationRef.current.hasStarted = true; setIsLoading(true); setError(null); // Inject auth into navigation store so Supabase queries work if (user?.id && accessToken) { navigationStore.setAuthInfo(accessToken, user.id); } // Initialize user node in profile context logger.debug('neo-user-context', '🔄 Starting context initialization'); // Initialize user node — race against 8s timeout so spinner never hangs const switchTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('switchContext timed out after 8000ms')), 8000) ) try { await Promise.race([ navigationStore.switchContext({ main: 'profile', base: 'profile', extended: 'overview' }, null, null), switchTimeout ]); const userNavigationNode = navigationStore.context.node; if (userNavigationNode?.id && userNavigationNode?.data) { const userNodeData: CCUserNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'User', uuid_string: userNavigationNode.id, node_storage_path: userNavigationNode.node_storage_path || '', title: String(userNavigationNode.data?.user_name || 'User'), user_name: String(userNavigationNode.data?.user_name || 'User'), user_email: user?.email || '', user_type: 'User', user_id: userNavigationNode.id, worker_node_data: JSON.stringify(userNavigationNode.data || {}) }; setUserNode(userNodeData); logger.debug('neo-user-context', '✅ User node loaded from navigation store'); } else if (userNavigationNode?.id) { logger.debug('neo-user-context', 'â„šī¸ User node exists but data not yet loaded - will retry later', { nodeId: userNavigationNode.id, hasData: !!userNavigationNode.data }); } else { logger.debug('neo-user-context', 'â„šī¸ No user node in navigation store yet - will retry later'); } } catch (navError) { logger.warn('neo-user-context', 'âš ī¸ Navigation store initialization failed - continuing without user node', { error: navError instanceof Error ? navError.message : String(navError) }); // Continue without user node - this is not critical for basic functionality } // Set final state — userDbName signals auth availability for UI guards setUserDbName(user?.id || null); setWorkerDbName(null); setIsInitialized(true); setIsLoading(false); initializationRef.current.isComplete = true; logger.debug('neo-user-context', '✅ Context initialization complete'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to initialize user context'; logger.error('neo-user-context', '❌ Failed to initialize context', { error: errorMessage }); setError(errorMessage); setIsLoading(false); setIsInitialized(true); initializationRef.current.isComplete = true; } }; initializeContext(); }, [user, profile, isUserInitialized, navigationStore, isInitialized]); // Calendar Navigation Functions const navigateToDay = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'calendar', extended: 'day' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCCalendarNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'CalendarDay', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, name: node.label, calendar_type: 'day', calendar_name: node.label, start_date: new Date().toISOString(), end_date: new Date().toISOString() }; setCurrentCalendarNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'CalendarDay', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to day'); } finally { setIsLoading(false); } }; const navigateToWeek = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'calendar', extended: 'week' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCCalendarNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'CalendarWeek', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, name: node.label, calendar_type: 'week', calendar_name: node.label, start_date: new Date().toISOString(), end_date: new Date().toISOString() }; setCurrentCalendarNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'CalendarWeek', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to week'); } finally { setIsLoading(false); } }; const navigateToMonth = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'calendar', extended: 'month' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCCalendarNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'CalendarMonth', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, name: node.label, calendar_type: 'month', calendar_name: node.label, start_date: new Date().toISOString(), end_date: new Date().toISOString() }; setCurrentCalendarNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'CalendarMonth', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to month'); } finally { setIsLoading(false); } }; const navigateToYear = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'calendar', extended: 'year' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCCalendarNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'CalendarYear', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, name: node.label, calendar_type: 'year', calendar_name: node.label, start_date: new Date().toISOString(), end_date: new Date().toISOString() }; setCurrentCalendarNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'CalendarYear', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to year'); } finally { setIsLoading(false); } }; // Worker Navigation Functions const navigateToTimetable = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'teaching', extended: 'timetable' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCUserTeacherTimetableNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'UserTeacherTimetable', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, school_db_name: workerDbName || '', school_timetable_id: id || node.id }; setCurrentWorkerNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'UserTeacherTimetable', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to timetable'); } finally { setIsLoading(false); } }; const navigateToJournal = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'teaching', extended: 'journal' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCUserTeacherTimetableNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'UserTeacherTimetable', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, school_db_name: workerDbName || '', school_timetable_id: id || node.id }; setCurrentWorkerNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'UserTeacherTimetable', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to journal'); } finally { setIsLoading(false); } }; const navigateToPlanner = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'teaching', extended: 'planner' }, null, null); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCUserTeacherTimetableNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'UserTeacherTimetable', uuid_string: id || node.id, node_storage_path: node.node_storage_path || '', title: node.label, school_db_name: workerDbName || '', school_timetable_id: id || node.id }; setCurrentWorkerNode({ id: id || node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'UserTeacherTimetable', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to planner'); } finally { setIsLoading(false); } }; const navigateToClass = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'teaching', extended: 'classes' }, null, null); await navigationStore.navigate(id, ''); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCUserTeacherTimetableNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'UserTeacherTimetable', uuid_string: node.id, node_storage_path: node.node_storage_path || '', title: node.label, school_db_name: workerDbName || '', school_timetable_id: node.id }; setCurrentWorkerNode({ id: node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'UserTeacherTimetable', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to class'); } finally { setIsLoading(false); } }; const navigateToLesson = async (id: string) => { if (!user?.id) return; setIsLoading(true); try { await navigationStore.switchContext({ base: 'teaching', extended: 'lessons' }, null, null); await navigationStore.navigate(id, ''); const node = navigationStore.context.node; if (node?.data) { const nodeData: CCUserTeacherTimetableNodeProps = { ...getBaseNodeProps(), __primarylabel__: 'UserTeacherTimetable', uuid_string: node.id, node_storage_path: node.node_storage_path || '', title: node.label, school_db_name: workerDbName || '', school_timetable_id: node.id }; setCurrentWorkerNode({ id: node.id, label: node.label, title: node.label, node_storage_path: node.node_storage_path || '', type: 'UserTeacherTimetable', nodeData }); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to navigate to lesson'); } finally { setIsLoading(false); } }; return ( {children} ); }; export const useNeoUser = () => useContext(NeoUserContext);