Initial commit

This commit is contained in:
2025-07-11 13:21:49 +00:00
commit 8a7ab3ac24
262 changed files with 28219 additions and 0 deletions
@@ -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!');
}
}
+58
View File
@@ -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;
}
}
+159
View File
@@ -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
});
}
}
+118
View File
@@ -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();
+78
View File
@@ -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;
}
}
}
+68
View File
@@ -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;
}
}
}
+256
View File
@@ -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
};
}
}
}
+390
View File
@@ -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;
}
}
}