import os from modules.logger_tool import initialise_logger logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) import modules.database.tools.neo4j_driver_tools as driver_tools import modules.database.tools.neo4j_session_tools as session_tools from datetime import datetime, timedelta from typing import List, Optional, Dict, Any def get_static_nodes(context: str, db_name: str) -> List[Dict[str, Any]]: """Get static nodes for a specific context.""" if context == 'workers': # For workers context, show teacher node first, then timetables and classes query = """ MATCH (t:Teacher) RETURN DISTINCT { id: t.uuid_string, path: t.node_storage_path, label: t.teacher_name_formal, type: 'Teacher', isStatic: true, order: 0, section: 'Root' } as node UNION ALL MATCH (t:UserTeacherTimetable) RETURN DISTINCT { id: t.uuid_string, path: t.node_storage_path, label: t.name, type: 'UserTeacherTimetable', isStatic: true, order: 1, section: 'Timetables' } as node UNION ALL MATCH (t:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class) RETURN DISTINCT { id: c.uuid_string, path: c.node_storage_path, label: c.name, type: 'Class', isStatic: true, order: 2, section: 'Classes' } as node """ elif context == 'user': # For user context, show the user node query = """ MATCH (u:User) RETURN DISTINCT { id: u.uuid_string, path: u.node_storage_path, label: u.user_name, type: 'User', isStatic: true, order: 0, section: 'Root' } as node """ else: # For calendar context, show today's calendar node first, then other calendar nodes today = datetime.now().strftime("%Y-%m-%d") query = """ MATCH (n:Calendar) WITH n, CASE WHEN date($today) >= date(n.start_date) AND date($today) <= date(n.end_date) THEN 0 ELSE 1 END as nodeOrder RETURN DISTINCT { id: n.uuid_string, path: n.node_storage_path, label: n.name, type: 'Calendar', isStatic: true, order: nodeOrder, section: CASE nodeOrder WHEN 0 THEN 'Today' ELSE 'Calendar' END } as node ORDER BY node.order, node.label """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, today=datetime.now().strftime("%Y-%m-%d")) return [record["node"] for record in result] except Exception as e: logger.error(f"Error getting static nodes: {str(e)}") return [] def get_today_calendar_node(db_name: str) -> Optional[Dict[str, Any]]: """Get today's calendar node.""" today = datetime.now().strftime("%Y-%m-%d") query = """ MATCH (n:Calendar) WHERE date($today) >= date(n.start_date) AND date($today) <= date(n.end_date) RETURN n.uuid_string as id, n.path as path, n.name as label, 'Calendar' as type LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, today=today) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting today's calendar node: {str(e)}") return None def get_relative_calendar_node(day_offset: int, db_name: str) -> Optional[Dict[str, Any]]: """Get calendar node relative to today.""" target_date = (datetime.now() + timedelta(days=day_offset)).strftime("%Y-%m-%d") query = """ MATCH (n:Calendar) WHERE date($target_date) >= date(n.start_date) AND date($target_date) <= date(n.end_date) RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label, 'Calendar' as type LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, target_date=target_date) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting relative calendar node: {str(e)}") return None def get_next_month_node(db_name: str) -> Optional[Dict[str, Any]]: """Get next month's calendar node.""" next_month_start = (datetime.now().replace(day=1) + timedelta(days=32)).replace(day=1).strftime("%Y-%m-%d") query = """ MATCH (n:Calendar) WHERE date($next_month_start) >= date(n.start_date) AND date($next_month_start) <= date(n.end_date) RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label, 'Calendar' as type LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, next_month_start=next_month_start) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting next month node: {str(e)}") return None def get_previous_month_node(db_name: str) -> Optional[Dict[str, Any]]: """Get previous month's calendar node.""" prev_month_start = (datetime.now().replace(day=1) - timedelta(days=1)).replace(day=1).strftime("%Y-%m-%d") query = """ MATCH (n:Calendar) WHERE date($prev_month_start) >= date(n.start_date) AND date($prev_month_start) <= date(n.end_date) RETURN n.uuid_string as id, n.node_storage_path as path, n.name as label, 'Calendar' as type LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, prev_month_start=prev_month_start) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting previous month node: {str(e)}") return None def get_user_timetables(db_name: str) -> List[Dict[str, Any]]: """Get user's timetables.""" query = """ MATCH (t:UserTeacherTimetable) RETURN t.uuid_string as id, t.node_storage_path as path, t.name as label, 'UserTeacherTimetable' as type """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query) return [dict(record) for record in result] except Exception as e: logger.error(f"Error getting user timetables: {str(e)}") return [] def get_timetable_classes(timetable_id: str, db_name: str) -> List[Dict[str, Any]]: """Get classes for a timetable.""" query = """ MATCH (t:UserTeacherTimetable {uuid_string: $timetable_id})-[:HAS_CLASS]->(c:Class) RETURN c.uuid_string as id, c.node_storage_path as path, c.name as label, 'Class' as type """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, timetable_id=timetable_id) return [dict(record) for record in result] except Exception as e: logger.error(f"Error getting timetable classes: {str(e)}") return [] def get_next_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get next lesson for a class.""" now = datetime.now().strftime("%Y-%m-%d %H:%M") query = """ MATCH (c:Class {uuid_string: $class_id})-[:HAS_LESSON]->(l:Lesson) WHERE l.start_time > $now RETURN l.uuid_string as id, l.node_storage_path as path, l.name as label, 'Lesson' as type ORDER BY l.start_time ASC LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, class_id=class_id, now=now) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting next lesson: {str(e)}") return None def get_previous_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get previous lesson for a class.""" now = datetime.now().strftime("%Y-%m-%d %H:%M") query = """ MATCH (c:Class {uuid_string: $class_id})-[:HAS_LESSON]->(l:Lesson) WHERE l.start_time < $now RETURN l.uuid_string as id, l.path as path, l.name as label, 'Lesson' as type ORDER BY l.start_time DESC LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, class_id=class_id, now=now) record = result.single() return dict(record) if record else None except Exception as e: logger.error(f"Error getting previous lesson: {str(e)}") return None def save_shared_snapshot(path: str, room_id: str, snapshot: Dict[str, Any]) -> bool: """Save snapshot to a shared room.""" try: # Save the snapshot to the shared room's storage session_tools.save_tldraw_node_file(path, room_id, snapshot) return True except Exception as e: logger.error(f"Error saving shared snapshot: {str(e)}") return False def get_connected_nodes_for_workers(node_id: str, db_name: str) -> List[Dict[str, Any]]: """Get connected nodes specific to the workers context.""" query = """ MATCH (n {uuid_string: $node_id}) WITH n CALL { WITH n MATCH (n:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class) RETURN c.uuid_string as id, c.node_storage_path as path, c.name as label, 'Class' as type UNION MATCH (n:Class)<-[:HAS_CLASS]-(t:UserTeacherTimetable) RETURN t.uuid_string as id, t.node_storage_path as path, t.name as label, 'UserTeacherTimetable' as type UNION MATCH (n:Class)-[:HAS_LESSON]->(l:Lesson) RETURN l.uuid_string as id, l.node_storage_path as path, l.name as label, 'Lesson' as type } RETURN DISTINCT id, path, label, type """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, node_id=node_id) return [dict(record) for record in result] except Exception as e: logger.error(f"Error getting connected nodes for workers: {str(e)}") return [] def get_connected_nodes(node_id: str, db_name: str, context: str = None) -> List[Dict[str, Any]]: """Get connected nodes based on context.""" if context == 'workers': return get_connected_nodes_for_workers(node_id, db_name) # Default query for other contexts query = """ MATCH (n {uuid_string: $node_id})-[r]-(connected) RETURN DISTINCT connected.uuid_string as id, connected.path as path, connected.name as label, labels(connected)[0] as type """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, node_id=node_id) return [dict(record) for record in result] except Exception as e: logger.error(f"Error getting connected nodes: {str(e)}") return [] ## Worker Navigation def get_worker_structure(db_name: str) -> Dict[str, Any]: """Get the complete worker structure including schools, departments, timetables, classes, and lessons.""" try: query = """ // Match all worker-related nodes MATCH (s:School) OPTIONAL MATCH (s)-[:HAS_DEPARTMENT]->(d:Department) OPTIONAL MATCH (d)-[:HAS_TIMETABLE]->(t:UserTeacherTimetable) OPTIONAL MATCH (t)-[:HAS_CLASS]->(c:Class) OPTIONAL MATCH (c)-[:HAS_LESSON]->(l:TimetableLesson) WITH s, d, t, c, l ORDER BY s.school_name, d.department_code, t.name, c.class_code, l.start_time // Collect all nodes RETURN { schools: collect(DISTINCT { id: s.uuid_string, path: s.node_storage_path, name: s.school_name, __primarylabel__: 'School' }), departments: collect(DISTINCT { id: d.uuid_string, path: d.node_storage_path, code: d.department_code, school_id: s.uuid_string, __primarylabel__: 'Department' }), timetables: collect(DISTINCT { id: t.uuid_string, path: t.node_storage_path, name: t.name, department_id: d.uuid_string, __primarylabel__: 'UserTeacherTimetable' }), classes: collect(DISTINCT { id: c.uuid_string, path: c.node_storage_path, code: c.class_code, timetable_id: t.uuid_string, __primarylabel__: 'Class' }), lessons: collect(DISTINCT { id: l.uuid_string, path: l.node_storage_path, start_time: l.start_time, class_id: c.uuid_string, __primarylabel__: 'TimetableLesson' }) } as structure """ with driver_tools.get_session(database=db_name) as session: result = session.run(query) record = result.single() if not record: logger.error('No worker structure found') return None return { "status": "success", "structure": record["structure"] } except Exception as e: logger.error(f"Error getting worker structure: {str(e)}") return None def get_school_node(school_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get a specific school node.""" query = """ MATCH (s:School {uuid_string: $school_id}) RETURN { id: s.uuid_string, path: s.node_storage_path, name: s.school_name, __primarylabel__: 'School' } as node """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, school_id=school_id) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting school node: {str(e)}") return None def get_department_node(dept_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get a specific department node.""" query = """ MATCH (d:Department {uuid_string: $dept_id}) RETURN { id: d.uuid_string, path: d.node_storage_path, code: d.department_code, __primarylabel__: 'Department' } as node """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, dept_id=dept_id) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting department node: {str(e)}") return None def get_timetable_node(timetable_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get a specific timetable node.""" query = """ MATCH (t:UserTeacherTimetable {uuid_string: $timetable_id}) RETURN { id: t.uuid_string, path: t.node_storage_path, name: t.name, __primarylabel__: 'UserTeacherTimetable' } as node """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, timetable_id=timetable_id) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting timetable node: {str(e)}") return None def get_class_node(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get a specific class node.""" query = """ MATCH (c:Class {uuid_string: $class_id}) RETURN { id: c.uuid_string, path: c.node_storage_path, code: c.class_code, __primarylabel__: 'Class' } as node """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, class_id=class_id) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting class node: {str(e)}") return None def get_lesson_node(lesson_id: str, db_name: str) -> Optional[Dict[str, Any]]: """Get a specific lesson node.""" query = """ MATCH (l:TimetableLesson {uuid_string: $lesson_id}) RETURN { id: l.uuid_string, path: l.node_storage_path, start_time: l.start_time, __primarylabel__: 'TimetableLesson' } as node """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, lesson_id=lesson_id) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting lesson node: {str(e)}") return None def get_current_lesson(db_name: str) -> Optional[Dict[str, Any]]: """Get the current or next upcoming lesson.""" now = datetime.now().strftime("%Y-%m-%d %H:%M") query = """ MATCH (l:TimetableLesson) WHERE l.start_time >= $now RETURN { id: l.uuid_string, path: l.node_storage_path, start_time: l.start_time, __primarylabel__: 'TimetableLesson' } as node ORDER BY l.start_time ASC LIMIT 1 """ try: with driver_tools.get_session(database=db_name) as session: result = session.run(query, now=now) record = result.single() return record["node"] if record else None except Exception as e: logger.error(f"Error getting current lesson: {str(e)}") return None