import axiosInstance from '../../axiosConfig'; import { formatEmailForDatabase } from './neoDBService'; import { CCUserNodeProps, CCTeacherNodeProps, CCCalendarNodeProps, CCUserTeacherTimetableNodeProps } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types'; import { NavigationNode, NodeContext } from '../../types/navigation'; import { TLBinding, TLShapeId } from '@tldraw/tldraw'; import { logger } from '../../debugConfig'; import { useNavigationStore } from '../../stores/navigationStore'; import { DatabaseNameService } from './databaseNameService'; // Dev configuration - only hardcoded value we need const DEV_SCHOOL_NAME = 'default'; const DEV_SCHOOL_GROUP = 'development' const ADMIN_USER_NAME = 'kcar'; const ADMIN_USER_GROUP = 'admin'; interface ShapeState { parentId: TLShapeId | null; isPageChild: boolean | null; hasChildren: boolean | null; bindings: TLBinding[] | null; } interface NodeResponse { status: string; nodes: { userNode: CCUserNodeProps; calendarNode: CCCalendarNodeProps; teacherNode: CCTeacherNodeProps; timetableNode: CCUserTeacherTimetableNodeProps; }; } interface NodeDataResponse { __primarylabel__: string; uuid_string: string; node_storage_path: string; created: string; merged: string; state: ShapeState | null; defaultComponent: boolean | null; user_name?: string; user_email?: string; user_type?: string; user_id?: string; worker_node_data?: string; [key: string]: string | number | boolean | null | ShapeState | undefined; } interface DefaultNodeResponse { status: string; node: { id: string; node_storage_path: string; type: string; label: string; data: NodeDataResponse; }; } export interface ProcessedUserNodes { privateUserNode: CCUserNodeProps; connectedNodes: { calendar?: CCCalendarNodeProps; teacher?: CCTeacherNodeProps; timetable?: CCUserTeacherTimetableNodeProps; }; } export interface CalendarStructureResponse { status: string; data: { currentDay: string; days: Record; weeks: Record; months: Record; years: { id: string; title: string; months: { id: string }[]; year: string; }[]; }; } export interface WorkerStructureResponse { status: string; data: { timetables: Record>; classes: Record>; lessons: Record>; journals: Record>; planners: Record>; }; } export class UserNeoDBService { static async fetchUserNodesData( email: string, userDbName?: string, workerDbName?: string ): Promise { try { if (!userDbName) { logger.error('neo4j-service', '❌ Attempted to fetch nodes without database name'); return null; } const formattedEmail = formatEmailForDatabase(email); const uniqueId = `User_${formattedEmail}`; logger.debug('neo4j-service', '🔄 Fetching user nodes data', { email, formattedEmail, userDbName, workerDbName, uniqueId }); // First get the user node from profile context const userNode = await this.getDefaultNode('profile', userDbName); if (!userNode || !userNode.data) { throw new Error('Failed to fetch user node or node data missing'); } logger.debug('neo4j-service', '✅ Found user node', { nodeId: userNode.id, type: userNode.type, hasData: !!userNode.data, userDbName, workerDbName }); // Initialize result structure const processedNodes: ProcessedUserNodes = { privateUserNode: { ...userNode.data, __primarylabel__: 'User' as const, title: userNode.data.user_email || 'User', w: 200, h: 200, headerColor: '#3e6589', backgroundColor: '#f0f0f0', isLocked: false } as CCUserNodeProps, connectedNodes: {} }; try { // Get calendar node from calendar context const calendarNode = await this.getDefaultNode('calendar', userDbName); if (calendarNode?.data) { processedNodes.connectedNodes.calendar = { ...calendarNode.data, __primarylabel__: 'Calendar' as const, title: calendarNode.data.calendar_name || 'Calendar', w: 200, h: 200, headerColor: '#3e6589', backgroundColor: '#f0f0f0', isLocked: false } as CCCalendarNodeProps; logger.debug('neo4j-service', '✅ Found calendar node', { nodeId: calendarNode.id, node_storage_path: calendarNode.data.node_storage_path }); } else { logger.debug('neo4j-service', 'â„šī¸ No calendar node found'); } } catch (error) { logger.warn('neo4j-service', 'âš ī¸ Failed to fetch calendar node:', error); // Continue without calendar node } // Get teacher node from teaching context if worker database is available if (workerDbName) { try { const teacherNode = await this.getDefaultNode('teaching', userDbName); if (teacherNode?.data) { processedNodes.connectedNodes.teacher = { ...teacherNode.data, __primarylabel__: 'Teacher' as const, title: teacherNode.data.teacher_name_formal || 'Teacher', w: 200, h: 200, headerColor: '#3e6589', backgroundColor: '#f0f0f0', isLocked: false, user_db_name: userDbName, school_db_name: workerDbName } as CCTeacherNodeProps; logger.debug('neo4j-service', '✅ Found teacher node', { nodeId: teacherNode.id, node_storage_path: teacherNode.data.node_storage_path, userDbName, workerDbName }); } else { logger.debug('neo4j-service', 'â„šī¸ No teacher node found'); } } catch (error) { logger.warn('neo4j-service', 'âš ī¸ Failed to fetch teacher node:', error); // Continue without teacher node } } logger.debug('neo4j-service', '✅ Processed all user nodes', { hasUserNode: !!processedNodes.privateUserNode, hasCalendar: !!processedNodes.connectedNodes.calendar, hasTeacher: !!processedNodes.connectedNodes.teacher, teacherData: processedNodes.connectedNodes.teacher ? { uuid_string: processedNodes.connectedNodes.teacher.uuid_string, school_db_name: processedNodes.connectedNodes.teacher.school_db_name, node_storage_path: processedNodes.connectedNodes.teacher.node_storage_path } : null }); return processedNodes; } catch (error: unknown) { if (error instanceof Error) { logger.error('neo4j-service', '❌ Failed to fetch user nodes:', error.message); } else { logger.error('neo4j-service', '❌ Failed to fetch user nodes:', String(error)); } throw error; } } static getUserDatabaseName(userType: string, identifier: string): string { return DatabaseNameService.getUserPrivateDB(userType, identifier); } static getSchoolDatabaseName(schoolId: string): string { return DatabaseNameService.getSchoolPrivateDB(schoolId); } static getDefaultSchoolDatabaseName(): string { return DatabaseNameService.getStoredSchoolDatabase() || ''; } static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeDataResponse } | null> { try { logger.debug('neo4j-service', '🔄 Fetching node data', { nodeId, dbName }); const response = await axiosInstance.get<{ status: string; node: { node_type: string; node_data: NodeDataResponse; }; }>('/database/tools/get-node', { params: { uuid_string: nodeId, db_name: dbName } }); if (response.data?.status === 'success' && response.data.node) { return response.data.node; } return null; } catch (error) { logger.error('neo4j-service', '❌ Failed to fetch node data:', error); throw error; } } static getNodeDatabaseName(node: NavigationNode): string { // Validate that node and node_storage_path exist if (!node || !node.node_storage_path) { logger.error('neo4j-service', '❌ Invalid node or missing node_storage_path', { node: node ? { id: node.id, type: node.type, label: node.label } : null, hasStoragePath: !!node?.node_storage_path }); throw new Error('Node is missing required storage path information'); } // If the node path starts with /node_filesystem/users/, it's in a user database if (node.node_storage_path.startsWith('users/')) { const parts = node.node_storage_path.split('/'); const databaseIndex = parts.indexOf('databases'); if (databaseIndex >= 0 && parts.length > databaseIndex + 1) { return parts[databaseIndex + 1]; } // parts[3] should be the database name (e.g., cc.users.surfacedashdev3atkevlaraidotcom) if (parts.length >= 4) { return parts[3]; } logger.warn('neo4j-service', 'âš ī¸ Unexpected user path format', { path: node.node_storage_path }); return 'cc.users'; } // For Supabase Storage paths (cc.public.snapshots/...), determine database based on node type if (node.node_storage_path.startsWith('cc.public.snapshots/')) { const parts = node.node_storage_path.split('/'); const nodeType = parts[1]; // e.g., 'User', 'Teacher', 'School' if (nodeType === 'User') { return DatabaseNameService.getStoredUserDatabase() || 'cc.users'; } else if (nodeType === 'Teacher' || nodeType === 'Student') { return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes'; } else if (nodeType === 'School') { return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes'; } } // For school/worker nodes, extract from the path or use a default if (node.node_storage_path.startsWith('schools/')) { const parts = node.node_storage_path.split('/'); const databaseIndex = parts.indexOf('databases'); if (databaseIndex >= 0 && parts.length > databaseIndex + 1) { return parts[databaseIndex + 1]; } if (parts.length >= 4) { return parts[3]; } const storedSchoolDb = DatabaseNameService.getStoredSchoolDatabase(); if (storedSchoolDb) { logger.warn('neo4j-service', 'âš ī¸ Falling back to stored school database name', { path: node.node_storage_path, storedSchoolDb }); return storedSchoolDb; } logger.warn('neo4j-service', 'âš ī¸ Could not determine school database from path', { path: node.node_storage_path }); return DatabaseNameService.getStoredSchoolDatabase() || 'cc.institutes'; } // Try to extract from path, but provide fallback const parts = node.node_storage_path.split('/'); if (parts.length >= 4) { return parts[3]; } // Default fallback logger.warn('neo4j-service', 'âš ī¸ Using fallback database name', { path: node.node_storage_path, nodeType: node.type }); return 'cc.users'; //TODO: remove hard-coding } static async getDefaultNode(context: NodeContext, dbName: string): Promise { try { logger.debug('neo4j-service', '🔄 Fetching default node', { context, dbName }); // For overview context, we need to extract the base context from the current navigation state const params: Record = { db_name: dbName }; if (context === 'overview') { // Get the current base context from the navigation store const navigationStore = useNavigationStore.getState(); params.base_context = navigationStore.context.base; } const response = await axiosInstance.get( `/database/tools/get-default-node/${context}`, { params } ); if (response.data?.status === 'success' && response.data.node) { return { id: response.data.node.id, node_storage_path: response.data.node.node_storage_path, type: response.data.node.type, label: response.data.node.label, data: response.data.node.data }; } return null; } catch (error) { logger.error('neo4j-service', '❌ Failed to fetch default node:', error); throw error; } } static async fetchCalendarStructure(dbName: string): Promise { try { logger.debug('navigation', '🔄 Fetching calendar structure', { dbName }); const response = await axiosInstance.get( `/database/calendar-structure/get-calendar-structure?db_name=${dbName}` ); if (response.data.status === 'success') { logger.info('navigation', '✅ Calendar structure fetched successfully'); return response.data.data; } throw new Error('Failed to fetch calendar structure'); } catch (error) { logger.error('navigation', '❌ Failed to fetch calendar structure:', error); throw error; } } static async fetchWorkerStructure(dbName: string): Promise { try { logger.debug('navigation', '🔄 Fetching worker structure', { dbName }); const response = await axiosInstance.get( `/database/worker-structure/get-worker-structure?db_name=${dbName}` ); if (response.data.status === 'success') { logger.info('navigation', '✅ Worker structure fetched successfully'); return response.data.data; } throw new Error('Failed to fetch worker structure'); } catch (error) { logger.error('navigation', '❌ Failed to fetch worker structure:', error); throw error; } } }