Initial commit
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
export async function uploadCurriculum(file: File, backendUrl: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/database/curriculum/upload-subject-curriculum`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
console.log(result);
|
||||
alert('Upload Successful!');
|
||||
} else {
|
||||
alert('Upload failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading curriculum:', error);
|
||||
alert('Upload failed!');
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadSubjectCurriculum(file: File, backendUrl: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/database/curriculum/upload-subject-curriculum`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const result = await response.json();
|
||||
console.log(result);
|
||||
alert('Upload Successful!');
|
||||
} else {
|
||||
alert('Upload failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading curriculum:', error);
|
||||
alert('Upload failed!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
export class DatabaseNameService {
|
||||
static readonly CC_USERS = 'cc.users';
|
||||
static readonly CC_SCHOOLS = 'cc.institutes';
|
||||
|
||||
static getUserPrivateDB(userType: string, username: string): string {
|
||||
const dbName = `${this.CC_USERS}.${userType}.${username}`;
|
||||
logger.debug('database-name-service', '📥 Generating user private DB name', {
|
||||
userType,
|
||||
username,
|
||||
dbName
|
||||
});
|
||||
return dbName;
|
||||
}
|
||||
|
||||
static getSchoolPrivateDB(schoolId: string): string {
|
||||
const dbName = `${this.CC_SCHOOLS}.${schoolId}`;
|
||||
logger.debug('database-name-service', '📥 Generating school private DB name', {
|
||||
schoolId,
|
||||
dbName
|
||||
});
|
||||
return dbName;
|
||||
}
|
||||
|
||||
static getDevelopmentSchoolDB(): string {
|
||||
const dbName = `${this.CC_SCHOOLS}.development.default`;
|
||||
logger.debug('database-name-service', '📥 Getting default school DB name', {
|
||||
dbName
|
||||
});
|
||||
return dbName;
|
||||
}
|
||||
|
||||
static getContextDatabase(context: string, userType: string, username: string): string {
|
||||
logger.debug('database-name-service', '📥 Resolving context database', {
|
||||
context,
|
||||
userType,
|
||||
username
|
||||
});
|
||||
|
||||
// For school-related contexts, use the schools database
|
||||
if (['school', 'department', 'class'].includes(context)) {
|
||||
logger.debug('database-name-service', '✅ Using schools database for context', {
|
||||
context,
|
||||
dbName: this.CC_SCHOOLS
|
||||
});
|
||||
return this.CC_SCHOOLS;
|
||||
}
|
||||
|
||||
// For user-specific contexts, use their private database
|
||||
const userDb = this.getUserPrivateDB(userType, username);
|
||||
logger.debug('database-name-service', '✅ Using user private database for context', {
|
||||
context,
|
||||
dbName: userDb
|
||||
});
|
||||
return userDb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Editor, createShapeId, IndexKey } from '@tldraw/tldraw';
|
||||
import axios from '../../axiosConfig';
|
||||
import { getShapeType, isValidNodeType, CCNodeTypes } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { AllNodeShapes, NodeShapeType, ShapeUtils } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-shapes';
|
||||
import { graphState } from '../../utils/tldraw/cc-base/cc-graph/graphStateUtil';
|
||||
import { NodeResponse, ConnectedNodesResponse } from '../../types/api';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
export class GraphNeoDBService {
|
||||
static async fetchConnectedNodesAndEdges(
|
||||
unique_id: string,
|
||||
db_name: string,
|
||||
editor: Editor
|
||||
) {
|
||||
try {
|
||||
logger.debug('graph-service', '📤 Fetching connected nodes', {
|
||||
unique_id,
|
||||
db_name
|
||||
});
|
||||
|
||||
const response = await axios.get<ConnectedNodesResponse>(
|
||||
'/database/tools/get-connected-nodes-and-edges', {
|
||||
params: {
|
||||
unique_id,
|
||||
db_name
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.status === "success") {
|
||||
// Make sure editor is set in graphState
|
||||
graphState.setEditor(editor);
|
||||
await this.processConnectedNodesResponse(response.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Failed to fetch connected nodes');
|
||||
} catch (error) {
|
||||
logger.error('graph-service', '❌ Failed to fetch connected nodes', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async processConnectedNodesResponse(
|
||||
data: ConnectedNodesResponse
|
||||
) {
|
||||
try {
|
||||
// Log the incoming data
|
||||
logger.debug('graph-service', '📥 Processing nodes response', {
|
||||
mainNode: data.main_node,
|
||||
connectedNodesCount: data.connected_nodes?.length,
|
||||
relationshipsCount: data.relationships?.length
|
||||
});
|
||||
|
||||
// Create a batch of nodes to process
|
||||
const nodesToProcess: NodeResponse['node_data'][] = [];
|
||||
|
||||
// Add connected nodes first
|
||||
if (data.connected_nodes) {
|
||||
data.connected_nodes.forEach(connectedNode => {
|
||||
if (isValidNodeType(connectedNode.type)) {
|
||||
// Convert the simplified node structure to node_data format
|
||||
const nodeData = {
|
||||
unique_id: connectedNode.id,
|
||||
tldraw_snapshot: connectedNode.tldraw_snapshot,
|
||||
name: connectedNode.label,
|
||||
__primarylabel__: connectedNode.type as keyof CCNodeTypes,
|
||||
created: new Date().toISOString(),
|
||||
merged: new Date().toISOString(),
|
||||
};
|
||||
nodesToProcess.push(nodeData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add main node last (if it exists) to ensure it's processed after connected nodes
|
||||
if (data.main_node) {
|
||||
nodesToProcess.push(data.main_node.node_data);
|
||||
}
|
||||
|
||||
// Process all nodes in batch
|
||||
for (const nodeData of nodesToProcess) {
|
||||
await this.createOrUpdateNode(nodeData);
|
||||
logger.debug('graph-service', '📝 Processed node', {
|
||||
nodeId: nodeData.unique_id,
|
||||
nodeType: nodeData.__primarylabel__
|
||||
});
|
||||
}
|
||||
|
||||
// After all nodes are processed, arrange them in grid
|
||||
graphState.arrangeNodesInGrid();
|
||||
|
||||
logger.debug('graph-service', '✅ Processed nodes batch', {
|
||||
processedCount: nodesToProcess.length,
|
||||
totalNodes: graphState.getAllNodes().length,
|
||||
nodesInState: Array.from(graphState.nodeData.keys())
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('graph-service', '❌ Failed to process connected nodes response', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async createOrUpdateNode(
|
||||
nodeData: NodeResponse['node_data']
|
||||
) {
|
||||
const uniqueId = nodeData.unique_id;
|
||||
const nodeType = nodeData.__primarylabel__;
|
||||
|
||||
if (!isValidNodeType(nodeType)) {
|
||||
logger.warn('graph-service', '⚠️ Unknown node type', { data: nodeData });
|
||||
return;
|
||||
}
|
||||
|
||||
const shapeType = getShapeType(nodeType) as NodeShapeType;
|
||||
|
||||
// Get the shape util for this node type
|
||||
const shapeUtil = ShapeUtils[shapeType];
|
||||
if (!shapeUtil) {
|
||||
logger.warn('graph-service', '⚠️ No shape util found for type', { type: shapeType });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get default props from the shape util's prototype
|
||||
const defaultProps = shapeUtil.prototype.getDefaultProps();
|
||||
|
||||
// Create the shape with proper typing based on the node type
|
||||
const shape = {
|
||||
id: createShapeId(uniqueId),
|
||||
type: shapeType,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
index: 'a1' as IndexKey,
|
||||
parentId: createShapeId('page:page'),
|
||||
isLocked: false,
|
||||
opacity: 1,
|
||||
meta: {},
|
||||
props: {
|
||||
...defaultProps,
|
||||
...nodeData,
|
||||
__primarylabel__: nodeData.__primarylabel__,
|
||||
unique_id: nodeData.unique_id,
|
||||
tldraw_snapshot: nodeData.path as string || '',
|
||||
}
|
||||
};
|
||||
|
||||
// Add to graphState
|
||||
graphState.addNode(shape as AllNodeShapes);
|
||||
|
||||
logger.debug('graph-service', '📝 Node processed', {
|
||||
uniqueId,
|
||||
nodeType,
|
||||
shapeId: shape.id,
|
||||
shapeType: shape.type
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { CCNodeTypes } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
export interface BaseNodeData {
|
||||
unique_id: string;
|
||||
path: string;
|
||||
__primarylabel__: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CalendarNodeData extends BaseNodeData {
|
||||
__primarylabel__: 'Calendar' | 'CalendarYear' | 'CalendarMonth' | 'CalendarWeek' | 'CalendarDay';
|
||||
date?: string;
|
||||
}
|
||||
|
||||
export interface WorkerNodeData extends BaseNodeData {
|
||||
__primarylabel__: 'School' | 'Department' | 'Teacher' | 'UserTeacherTimetable' | 'Class' | 'TimetableLesson';
|
||||
name?: string;
|
||||
teacher_code?: string;
|
||||
teacher_name_formal?: string;
|
||||
class_code?: string;
|
||||
department_code?: string;
|
||||
school_name?: string;
|
||||
}
|
||||
|
||||
export type NodeType = keyof CCNodeTypes | 'User' | 'Calendar' | 'CalendarYear' | 'CalendarMonth' | 'CalendarWeek' | 'CalendarDay' | 'Teacher' | 'UserTeacherTimetable' | 'Student' | 'Class' | 'TimetableLesson';
|
||||
|
||||
export function formatEmailForDatabase(email: string): string {
|
||||
// Convert to lowercase and replace special characters
|
||||
const sanitized = email.toLowerCase()
|
||||
.replace('@', 'at')
|
||||
.replace(/\./g, 'dot')
|
||||
.replace(/_/g, 'underscore')
|
||||
.replace(/-/g, 'dash');
|
||||
|
||||
// Add prefix and ensure no consecutive dashes
|
||||
return `${sanitized}`;
|
||||
}
|
||||
|
||||
export function generateNodeTitle(nodeData: BaseNodeData): string {
|
||||
try {
|
||||
const calendarData = nodeData as CalendarNodeData;
|
||||
const workerData = nodeData as WorkerNodeData;
|
||||
|
||||
switch (nodeData.__primarylabel__ as NodeType) {
|
||||
// Calendar nodes
|
||||
case 'Calendar':
|
||||
return 'Calendar';
|
||||
case 'CalendarYear':
|
||||
if (!calendarData.date) return 'Unknown Year';
|
||||
return `Year ${new Date(calendarData.date).getFullYear()}`;
|
||||
case 'CalendarMonth':
|
||||
if (!calendarData.date) return 'Unknown Month';
|
||||
return new Date(calendarData.date).toLocaleString('default', { month: 'long' });
|
||||
case 'CalendarWeek':
|
||||
if (!calendarData.date) return 'Unknown Week';
|
||||
return `Week ${new Date(calendarData.date).getDate()}`;
|
||||
case 'CalendarDay':
|
||||
if (!calendarData.date) return 'Unknown Day';
|
||||
return new Date(calendarData.date).toLocaleDateString();
|
||||
|
||||
// Worker/School nodes
|
||||
case 'School':
|
||||
return workerData.school_name || 'School';
|
||||
case 'Department':
|
||||
return workerData.department_code || 'Department';
|
||||
case 'Teacher':
|
||||
return workerData.teacher_name_formal || workerData.teacher_code || 'Teacher';
|
||||
case 'UserTeacherTimetable':
|
||||
return 'Timetable';
|
||||
case 'Class':
|
||||
return workerData.class_code || 'Class';
|
||||
case 'TimetableLesson':
|
||||
return 'Lesson';
|
||||
|
||||
default:
|
||||
logger.warn('neo4j-service', `⚠️ Unknown node type for title generation: ${nodeData.__primarylabel__}`);
|
||||
return 'Unknown Node';
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('neo4j-service', '❌ Failed to generate node title', { error, nodeData });
|
||||
return 'Error: Invalid Node Data';
|
||||
}
|
||||
}
|
||||
|
||||
export function getMonthFromWeek(weekDate: string): string {
|
||||
// Get the month that contains the most days of this week
|
||||
const weekStart = new Date(weekDate);
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||
|
||||
// If week spans two months, use the month that contains more days of the week
|
||||
if (weekStart.getMonth() !== weekEnd.getMonth()) {
|
||||
const daysInFirstMonth = new Date(weekStart.getFullYear(), weekStart.getMonth() + 1, 0).getDate() - weekStart.getDate() + 1;
|
||||
const daysInSecondMonth = 7 - daysInFirstMonth;
|
||||
|
||||
return daysInFirstMonth >= daysInSecondMonth ?
|
||||
weekStart.toLocaleString('default', { month: 'long' }) :
|
||||
weekEnd.toLocaleString('default', { month: 'long' });
|
||||
}
|
||||
|
||||
return weekStart.toLocaleString('default', { month: 'long' });
|
||||
}
|
||||
|
||||
export function getDatabaseName(path: string, defaultSchoolUuid = 'kevlarai'): string {
|
||||
// If the path starts with /node_filesystem/users/, it's in a user database
|
||||
if (path.startsWith('/node_filesystem/users/')) {
|
||||
const parts = path.split('/');
|
||||
// parts[3] should be the database name (e.g., cc.users.surfacedashdev3atkevlaraidotcom)
|
||||
return parts[3];
|
||||
}
|
||||
// For school/worker nodes, extract from the path or use default
|
||||
if (path.includes('/schools/')) {
|
||||
return `cc.institutes.${defaultSchoolUuid}`;
|
||||
}
|
||||
// Default to user database if we can't determine
|
||||
return path.split('/')[3];
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { supabase } from '../../supabaseClient';
|
||||
import { CCUser } from '../auth/authService';
|
||||
import { CCSchoolNodeProps, CCUserNodeProps } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { storageService, StorageKeys } from '../auth/localStorageService';
|
||||
import axiosInstance from '../../axiosConfig';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
// Dev configuration - only hardcoded value we need
|
||||
const DEV_SCHOOL_UUID = 'kevlarai';
|
||||
|
||||
class NeoRegistrationService {
|
||||
private static instance: NeoRegistrationService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): NeoRegistrationService {
|
||||
if (!NeoRegistrationService.instance) {
|
||||
NeoRegistrationService.instance = new NeoRegistrationService();
|
||||
}
|
||||
return NeoRegistrationService.instance;
|
||||
}
|
||||
|
||||
async registerNeo4JUser(
|
||||
user: CCUser,
|
||||
username: string,
|
||||
role: string
|
||||
): Promise<CCUserNodeProps> {
|
||||
try {
|
||||
// For teachers and students, fetch school node first
|
||||
let schoolNode = null;
|
||||
if (role.includes('teacher') || role.includes('student')) {
|
||||
schoolNode = await this.fetchSchoolNode(DEV_SCHOOL_UUID);
|
||||
if (!schoolNode) {
|
||||
throw new Error('Failed to fetch required school node');
|
||||
}
|
||||
}
|
||||
|
||||
// Create FormData with proper headers
|
||||
const formData = new FormData();
|
||||
|
||||
// Required fields
|
||||
formData.append('user_id', user.id);
|
||||
formData.append('user_type', role);
|
||||
formData.append('user_name', username);
|
||||
formData.append('user_email', user.email || '');
|
||||
|
||||
// Add school data if we have a school node
|
||||
if (schoolNode) {
|
||||
formData.append('school_uuid', schoolNode.school_uuid);
|
||||
formData.append('school_name', schoolNode.school_name);
|
||||
formData.append('school_website', schoolNode.school_website);
|
||||
formData.append('school_tldraw_snapshot', schoolNode.tldraw_snapshot);
|
||||
|
||||
// Add worker data based on role
|
||||
const workerData = role.includes('teacher') ? {
|
||||
teacher_code: username,
|
||||
teacher_name_formal: username,
|
||||
teacher_email: user.email,
|
||||
} : {
|
||||
student_code: username,
|
||||
student_name_formal: username,
|
||||
student_email: user.email,
|
||||
};
|
||||
|
||||
formData.append('worker_data', JSON.stringify(workerData));
|
||||
}
|
||||
|
||||
// Debug log the form data
|
||||
logger.debug('neo4j-service', '🔄 Sending form data', {
|
||||
userId: user.id,
|
||||
userType: role,
|
||||
userName: username,
|
||||
userEmail: user.email,
|
||||
schoolNode: schoolNode ? {
|
||||
uuid: schoolNode.school_uuid,
|
||||
name: schoolNode.school_name
|
||||
} : null
|
||||
});
|
||||
|
||||
const response = await axiosInstance.post('/database/entity/create-user', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.status !== 'success') {
|
||||
throw new Error(`Failed to create user: ${JSON.stringify(response.data)}`);
|
||||
}
|
||||
|
||||
const userNode = response.data.data.user_node;
|
||||
const workerNode = response.data.data.worker_node;
|
||||
|
||||
// Store calendar data if needed
|
||||
if (response.data.data.calendar_nodes) {
|
||||
logger.debug('neo4j-service', '🔄 Storing calendar data', {
|
||||
calendarNodes: response.data.data.calendar_nodes
|
||||
});
|
||||
storageService.set(StorageKeys.CALENDAR_DATA, response.data.data.calendar_nodes);
|
||||
}
|
||||
|
||||
// Update user node with worker data
|
||||
userNode.worker_node_data = JSON.stringify(workerNode);
|
||||
|
||||
await this.updateUserNeo4jDetails(user.id, userNode);
|
||||
|
||||
logger.info('neo4j-service', '✅ Neo4j user registration successful', {
|
||||
userId: user.id,
|
||||
nodeId: userNode.unique_id,
|
||||
hasCalendar: !!response.data.data.calendar_nodes
|
||||
});
|
||||
|
||||
return userNode;
|
||||
} catch (error) {
|
||||
logger.error('neo4j-service', '❌ Neo4j user registration failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserNeo4jDetails(userId: string, userNode: CCUserNodeProps) {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
metadata: {
|
||||
...userNode
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', userId);
|
||||
|
||||
if (error) {
|
||||
logger.error('neo4j-service', '❌ Failed to update Neo4j details:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSchoolNode(schoolUuid: string): Promise<CCSchoolNodeProps> {
|
||||
logger.debug('neo4j-service', '🔄 Fetching school node', { schoolUuid });
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.get(`/database/tools/get-school-node?school_uuid=${schoolUuid}`);
|
||||
|
||||
if (response.data?.status === 'success' && response.data.school_node) {
|
||||
logger.info('neo4j-service', '✅ School node fetched successfully');
|
||||
return response.data.school_node;
|
||||
}
|
||||
|
||||
throw new Error('Failed to fetch school node: ' + JSON.stringify(response.data));
|
||||
} catch (error) {
|
||||
logger.error('neo4j-service', '❌ Failed to fetch school node:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const neoRegistrationService = NeoRegistrationService.getInstance();
|
||||
@@ -0,0 +1,78 @@
|
||||
import { NavigationNode } from '../../types/navigation';
|
||||
import { getShapeType, CCNodeTypes } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { getThemeFromLabel } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-styles';
|
||||
import { logger } from '../../debugConfig';
|
||||
import { NodeData } from '../../types/graph-shape';
|
||||
|
||||
export class NeoShapeService {
|
||||
private static readonly DATE_TIME_FIELDS = [
|
||||
'merged', 'created', 'start_date', 'end_date', 'start_time', 'end_time'
|
||||
] as const;
|
||||
|
||||
private static processDateTimeFields(data: Record<string, unknown>): Record<string, unknown> {
|
||||
const processed = { ...data };
|
||||
for (const key of Object.keys(processed)) {
|
||||
if (this.DATE_TIME_FIELDS.includes(key as typeof this.DATE_TIME_FIELDS[number]) &&
|
||||
processed[key] &&
|
||||
typeof processed[key] === 'object') {
|
||||
processed[key] = processed[key].toString();
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
static getShapeConfig(node: NavigationNode, nodeData: NodeData, centerX: number, centerY: number) {
|
||||
try {
|
||||
// Get the shape type based on the node type
|
||||
const shapeType = getShapeType(node.type as keyof CCNodeTypes);
|
||||
|
||||
// Get theme colors based on the node type
|
||||
const theme = getThemeFromLabel(node.type);
|
||||
|
||||
// Default dimensions
|
||||
const width = 500;
|
||||
const height = 350;
|
||||
|
||||
// Process the node data
|
||||
const processedProps = {
|
||||
...this.processDateTimeFields(nodeData),
|
||||
title: nodeData.title || node.label,
|
||||
w: width,
|
||||
h: height,
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: true,
|
||||
hasChildren: null,
|
||||
bindings: null
|
||||
},
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
unique_id: node.id,
|
||||
tldraw_snapshot: node.tldraw_snapshot
|
||||
};
|
||||
|
||||
logger.debug('neo-shape-service', '📄 Created shape configuration', {
|
||||
nodeId: node.id,
|
||||
shapeType,
|
||||
theme,
|
||||
props: processedProps
|
||||
});
|
||||
|
||||
return {
|
||||
type: shapeType,
|
||||
x: centerX - (width / 2),
|
||||
y: centerY - (height / 2),
|
||||
props: processedProps
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('neo-shape-service', '❌ Failed to create shape configuration', {
|
||||
nodeId: node.id,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import axiosInstance from '../../axiosConfig';
|
||||
import { CCSchoolNodeProps } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { logger } from '../../debugConfig';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
interface CreateSchoolResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class SchoolNeoDBService {
|
||||
static async createSchools(
|
||||
): Promise<CreateSchoolResponse> {
|
||||
logger.warn('school-service', '📤 Creating schools using default config.yaml');
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.post(
|
||||
'/database/entity/create-schools',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.status === 'success' || response.data.status === 'Accepted') {
|
||||
logger.info('school-service', '✅ Schools successfully');
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Schools created successfully'
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(response.data.message || 'Creation failed');
|
||||
} catch (err: unknown) {
|
||||
const error = err as AxiosError;
|
||||
logger.error('school-service', '❌ Failed to create school', {
|
||||
error: error.message,
|
||||
details: error.response?.data
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getSchoolNode(schoolDbName: string): Promise<CCSchoolNodeProps | null> {
|
||||
logger.debug('school-service', '🔄 Fetching school node', { schoolDbName });
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.get(`/database/tools/get-default-node/school?db_name=${schoolDbName}`);
|
||||
|
||||
if (response.data?.status === 'success' && response.data.node) {
|
||||
logger.info('school-service', '✅ School node fetched successfully');
|
||||
return response.data.node;
|
||||
}
|
||||
|
||||
logger.warn('school-service', '⚠️ No school node found');
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||
logger.warn('school-service', '⚠️ School node not found (404)', { schoolDbName });
|
||||
return null;
|
||||
}
|
||||
logger.error('school-service', '❌ Failed to fetch school node:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import axios from '../../axiosConfig';
|
||||
import { CCTeacherNodeProps, CCUserNodeProps } from '../../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import { logger } from '../../debugConfig';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
interface UploadTimetableResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TeacherTimetableEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string;
|
||||
extendedProps: {
|
||||
subjectClass: string;
|
||||
color: string;
|
||||
periodCode: string;
|
||||
tldraw_snapshot?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class TimetableNeoDBService {
|
||||
static async uploadWorkerTimetable(
|
||||
file: File,
|
||||
userNode: CCUserNodeProps,
|
||||
workerNode: CCTeacherNodeProps
|
||||
): Promise<UploadTimetableResponse> {
|
||||
logger.debug('timetable-service', '📤 Uploading timetable', {
|
||||
fileName: file.name,
|
||||
schoolDbName: workerNode.school_db_name,
|
||||
userDbName: workerNode.user_db_name,
|
||||
teacherCode: workerNode.teacher_code
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('user_node', JSON.stringify({
|
||||
unique_id: userNode.unique_id,
|
||||
user_id: userNode.user_id,
|
||||
user_type: userNode.user_type,
|
||||
user_name: userNode.user_name,
|
||||
user_email: userNode.user_email,
|
||||
tldraw_snapshot: userNode.tldraw_snapshot,
|
||||
worker_node_data: userNode.worker_node_data
|
||||
|
||||
}));
|
||||
formData.append('worker_node', JSON.stringify({
|
||||
unique_id: workerNode.unique_id,
|
||||
teacher_code: workerNode.teacher_code,
|
||||
teacher_name_formal: workerNode.teacher_name_formal,
|
||||
teacher_email: workerNode.teacher_email,
|
||||
tldraw_snapshot: workerNode.tldraw_snapshot,
|
||||
worker_db_name: workerNode.school_db_name,
|
||||
user_db_name: workerNode.user_db_name
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'/database/timetables/upload-worker-timetable',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.status === 'success' || response.data.status === 'Accepted') {
|
||||
logger.info('timetable-service', '✅ Timetable upload successful');
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Timetable uploaded successfully'
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(response.data.message || 'Upload failed');
|
||||
} catch (err: unknown) {
|
||||
const error = err as AxiosError;
|
||||
logger.error('timetable-service', '❌ Failed to upload timetable', {
|
||||
error: error.message,
|
||||
details: error.response?.data
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchTeacherTimetableEvents(
|
||||
unique_id: string,
|
||||
school_db_name: string
|
||||
): Promise<TeacherTimetableEvent[]> {
|
||||
try {
|
||||
logger.debug('timetable-service', '📤 Fetching timetable events', {
|
||||
unique_id,
|
||||
school_db_name
|
||||
});
|
||||
|
||||
const response = await axios.get('/calendar/get_teacher_timetable_events', {
|
||||
params: {
|
||||
unique_id,
|
||||
school_db_name
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('timetable-service', '📥 Received response', {
|
||||
status: response.status,
|
||||
data: response.data
|
||||
});
|
||||
|
||||
if (response.data.status === "success") {
|
||||
return response.data.events;
|
||||
}
|
||||
throw new Error(response.data.message || 'Failed to fetch events');
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
logger.error('timetable-service', '❌ Failed to fetch timetable events', {
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
logger.error('timetable-service', '❌ Failed to fetch timetable events', { error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static lightenColor(color: string, amount: number): string {
|
||||
color = color.replace(/^#/, '');
|
||||
const num = parseInt(color, 16);
|
||||
const r = Math.min(255, (num >> 16) + amount);
|
||||
const g = Math.min(255, ((num >> 8) & 0x00FF) + amount);
|
||||
const b = Math.min(255, (num & 0x0000FF) + amount);
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
static getContrastColor(hexColor: string): string {
|
||||
hexColor = hexColor.replace(/^#/, '');
|
||||
const r = parseInt(hexColor.slice(0, 2), 16);
|
||||
const g = parseInt(hexColor.slice(2, 4), 16);
|
||||
const b = parseInt(hexColor.slice(4, 6), 16);
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
return luminance > 0.7 ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
static getEventRange(events: TeacherTimetableEvent[]) {
|
||||
if (events.length === 0) {
|
||||
return { start: null, end: null };
|
||||
}
|
||||
|
||||
let start = new Date(events[0].start);
|
||||
let end = new Date(events[0].end);
|
||||
|
||||
events.forEach(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
if (eventStart < start) {
|
||||
start = eventStart;
|
||||
}
|
||||
if (eventEnd > end) {
|
||||
end = eventEnd;
|
||||
}
|
||||
});
|
||||
|
||||
start.setDate(1);
|
||||
end.setMonth(end.getMonth() + 1, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
static getSubjectClassColor(subjectClass: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < subjectClass.length; i++) {
|
||||
hash = subjectClass.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return `hsl(${hash % 360}, 70%, 50%)`;
|
||||
}
|
||||
|
||||
static async handleTimetableUpload(
|
||||
file: File | undefined,
|
||||
userNode: CCUserNodeProps | undefined,
|
||||
workerNode: CCTeacherNodeProps | undefined
|
||||
): Promise<UploadResult> {
|
||||
if (!file) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No file selected'
|
||||
};
|
||||
}
|
||||
|
||||
if (!file.name.endsWith('.xlsx')) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Please upload an Excel (.xlsx) file'
|
||||
};
|
||||
}
|
||||
|
||||
if (!userNode) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'User information not found. Please ensure you are logged in as a user.'
|
||||
};
|
||||
}
|
||||
|
||||
if (!workerNode) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Teacher information not found. Please ensure you are logged in as a teacher.'
|
||||
};
|
||||
}
|
||||
|
||||
// Validate worker node has required fields
|
||||
const requiredWorkerFields = ['unique_id', 'teacher_code', 'teacher_name_formal', 'teacher_email', 'worker_db_name', 'path'];
|
||||
const requiredUserFields = ['unique_id', 'user_id', 'user_type', 'user_name', 'user_email', 'path', 'worker_node_data'];
|
||||
const missingWorkerFields = requiredWorkerFields.filter(field => !(field in workerNode));
|
||||
const missingUserFields = requiredUserFields.filter(field => !(field in userNode));
|
||||
|
||||
|
||||
if (missingWorkerFields.length > 0) {
|
||||
logger.error('timetable-service', '❌ Missing required teacher fields:', { missingWorkerFields });
|
||||
return {
|
||||
success: false,
|
||||
message: `Missing required teacher information: ${missingWorkerFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
if (missingUserFields.length > 0) {
|
||||
logger.error('timetable-service', '❌ Missing required user fields:', { missingUserFields });
|
||||
return {
|
||||
success: false,
|
||||
message: `Missing required user information: ${missingUserFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.uploadWorkerTimetable(file, userNode, workerNode);
|
||||
return {
|
||||
success: true,
|
||||
message: result.message
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to upload timetable';
|
||||
logger.error('timetable-service', '❌ Timetable upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
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_UUID = 'kevlarai';
|
||||
|
||||
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;
|
||||
unique_id: string;
|
||||
tldraw_snapshot: 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;
|
||||
tldraw_snapshot: 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<string, {
|
||||
id: string;
|
||||
date: string;
|
||||
title: string;
|
||||
}>;
|
||||
weeks: Record<string, {
|
||||
id: string;
|
||||
title: string;
|
||||
days: { id: string }[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}>;
|
||||
months: Record<string, {
|
||||
id: string;
|
||||
title: string;
|
||||
days: { id: string }[];
|
||||
weeks: { id: string }[];
|
||||
year: string;
|
||||
month: string;
|
||||
}>;
|
||||
years: {
|
||||
id: string;
|
||||
title: string;
|
||||
months: { id: string }[];
|
||||
year: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkerStructureResponse {
|
||||
status: string;
|
||||
data: {
|
||||
timetables: Record<string, Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>>;
|
||||
classes: Record<string, Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}>>;
|
||||
lessons: Record<string, Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}>>;
|
||||
journals: Record<string, Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
}>>;
|
||||
planners: Record<string, Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
}>>;
|
||||
};
|
||||
}
|
||||
|
||||
export class UserNeoDBService {
|
||||
static async fetchUserNodesData(
|
||||
email: string,
|
||||
userDbName?: string,
|
||||
workerDbName?: string
|
||||
): Promise<ProcessedUserNodes | null> {
|
||||
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,
|
||||
tldraw_snapshot: calendarNode.data.tldraw_snapshot
|
||||
});
|
||||
} 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,
|
||||
tldraw_snapshot: teacherNode.data.tldraw_snapshot,
|
||||
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 ? {
|
||||
unique_id: processedNodes.connectedNodes.teacher.unique_id,
|
||||
school_db_name: processedNodes.connectedNodes.teacher.school_db_name,
|
||||
tldraw_snapshot: processedNodes.connectedNodes.teacher.tldraw_snapshot
|
||||
} : 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, username: string): string {
|
||||
return DatabaseNameService.getUserPrivateDB(userType, username);
|
||||
}
|
||||
|
||||
static getSchoolDatabaseName(schoolId: string): string {
|
||||
return DatabaseNameService.getSchoolPrivateDB(schoolId);
|
||||
}
|
||||
|
||||
static getDefaultSchoolDatabaseName(): string {
|
||||
return DatabaseNameService.getDevelopmentSchoolDB();
|
||||
}
|
||||
|
||||
static async fetchNodeData(nodeId: string, dbName: string): Promise<{ node_type: string; node_data: NodeResponse['nodes']['userNode'] } | null> {
|
||||
try {
|
||||
logger.debug('neo4j-service', '🔄 Fetching node data', { nodeId, dbName });
|
||||
|
||||
const response = await axiosInstance.get<{
|
||||
status: string;
|
||||
node: {
|
||||
node_type: string;
|
||||
node_data: NodeResponse['nodes']['userNode'];
|
||||
};
|
||||
}>('/database/tools/get-node', {
|
||||
params: {
|
||||
unique_id: 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 {
|
||||
// If the node path starts with /node_filesystem/users/, it's in a user database
|
||||
if (node.tldraw_snapshot.startsWith('/node_filesystem/users/')) {
|
||||
const parts = node.tldraw_snapshot.split('/');
|
||||
// parts[3] should be the database name (e.g., cc.users.surfacedashdev3atkevlaraidotcom)
|
||||
return parts[3];
|
||||
}
|
||||
// For school/worker nodes, extract from the path or use a default
|
||||
if (node.tldraw_snapshot.includes('/schools/')) {
|
||||
return `cc.institutes.${DEV_SCHOOL_UUID}`;
|
||||
}
|
||||
// Default to user database if we can't determine
|
||||
return node.tldraw_snapshot.split('/')[3];
|
||||
}
|
||||
|
||||
static async getDefaultNode(context: NodeContext, dbName: string): Promise<NavigationNode | null> {
|
||||
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<string, string> = { 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<DefaultNodeResponse>(
|
||||
`/database/tools/get-default-node/${context}`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
if (response.data?.status === 'success' && response.data.node) {
|
||||
return {
|
||||
id: response.data.node.id,
|
||||
tldraw_snapshot: response.data.node.tldraw_snapshot,
|
||||
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<CalendarStructureResponse['data']> {
|
||||
try {
|
||||
logger.debug('navigation', '🔄 Fetching calendar structure', { dbName });
|
||||
|
||||
const response = await axiosInstance.get<CalendarStructureResponse>(
|
||||
`/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<WorkerStructureResponse['data']> {
|
||||
try {
|
||||
logger.debug('navigation', '🔄 Fetching worker structure', { dbName });
|
||||
|
||||
const response = await axiosInstance.get<WorkerStructureResponse>(
|
||||
`/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user