Compare commits
12 Commits
6322e046c5
...
8440b7c30d
| Author | SHA1 | Date | |
|---|---|---|---|
| 8440b7c30d | |||
| 74c2783b1a | |||
| fcd82eb5c9 | |||
| c654986f65 | |||
| f4ab617c59 | |||
| 9c36179f29 | |||
| f292cf1ce5 | |||
| 3a20ea0282 | |||
| 44986bfa23 | |||
| 41195a44cb | |||
| e1cd23230d | |||
| 77095e91b6 |
Binary file not shown.
416
app.py
416
app.py
@@ -9,7 +9,6 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField
|
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField, HiddenField
|
||||||
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
|
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -47,7 +46,6 @@ app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
|||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) # Langlebige Session für Dark Mode-Einstellung
|
||||||
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
|
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', os.path.join(os.getcwd(), 'uploads'))
|
||||||
app.config['WTF_CSRF_ENABLED'] = False
|
|
||||||
|
|
||||||
# OpenAI API-Konfiguration
|
# OpenAI API-Konfiguration
|
||||||
api_key = os.environ.get("OPENAI_API_KEY", "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")
|
api_key = os.environ.get("OPENAI_API_KEY", "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")
|
||||||
@@ -122,39 +120,7 @@ socketio = SocketIO(app)
|
|||||||
|
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
|
# Funktion zum Erstellen von Standardbenutzern
|
||||||
def initialize_app():
|
|
||||||
"""Initialisierung der Anwendung"""
|
|
||||||
print("Initialisierung der Anwendung...")
|
|
||||||
with app.app_context():
|
|
||||||
# Prüfen, ob die Datenbank existiert und initialisiert ist
|
|
||||||
try:
|
|
||||||
# Prüfen, ob Tabellen existieren
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Prüfen, ob Stammdaten vorhanden sind
|
|
||||||
if User.query.count() == 0:
|
|
||||||
print("Erstelle Standardbenutzer...")
|
|
||||||
create_default_users()
|
|
||||||
|
|
||||||
if Category.query.count() == 0:
|
|
||||||
print("Erstelle Standardkategorien...")
|
|
||||||
create_default_categories()
|
|
||||||
|
|
||||||
if MindMapNode.query.count() == 0 and Category.query.count() > 0:
|
|
||||||
print("Erstelle Beispiel-Mindmap...")
|
|
||||||
create_sample_mindmap()
|
|
||||||
|
|
||||||
print("Datenbank wurde erfolgreich initialisiert.")
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
print(f"Fehler bei der Datenbankinitialisierung: {e}")
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
# Moderne Methode für die Datenbankinitialisierung in Flask 2.2+
|
|
||||||
with app.app_context():
|
|
||||||
initialize_app()
|
|
||||||
|
|
||||||
def create_default_users():
|
def create_default_users():
|
||||||
"""Erstellt Standardbenutzer für die Anwendung"""
|
"""Erstellt Standardbenutzer für die Anwendung"""
|
||||||
users = [
|
users = [
|
||||||
@@ -765,7 +731,94 @@ def delete_mindmap(mindmap_id):
|
|||||||
flash('Mindmap erfolgreich gelöscht!', 'success')
|
flash('Mindmap erfolgreich gelöscht!', 'success')
|
||||||
return redirect(url_for('profile'))
|
return redirect(url_for('profile'))
|
||||||
|
|
||||||
# API-Endpunkte für Mindmap-Daten
|
# API-Endpunkte für UserMindmap CRUD-Operationen
|
||||||
|
@app.route('/api/mindmaps', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def api_create_user_mindmap():
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get('name')
|
||||||
|
description = data.get('description')
|
||||||
|
is_private = data.get('is_private', True)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({'error': 'Name ist erforderlich'}), 400
|
||||||
|
|
||||||
|
new_mindmap = UserMindmap(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
user_id=current_user.id,
|
||||||
|
is_private=is_private
|
||||||
|
)
|
||||||
|
db.session.add(new_mindmap)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({
|
||||||
|
'id': new_mindmap.id,
|
||||||
|
'name': new_mindmap.name,
|
||||||
|
'description': new_mindmap.description,
|
||||||
|
'is_private': new_mindmap.is_private,
|
||||||
|
'user_id': new_mindmap.user_id,
|
||||||
|
'created_at': new_mindmap.created_at.isoformat(),
|
||||||
|
'last_modified': new_mindmap.last_modified.isoformat()
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
@app.route('/api/mindmaps', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def api_get_user_mindmaps():
|
||||||
|
mindmaps = UserMindmap.query.filter_by(user_id=current_user.id).all()
|
||||||
|
return jsonify([{
|
||||||
|
'id': m.id,
|
||||||
|
'name': m.name,
|
||||||
|
'description': m.description,
|
||||||
|
'is_private': m.is_private,
|
||||||
|
'created_at': m.created_at.isoformat(),
|
||||||
|
'last_modified': m.last_modified.isoformat()
|
||||||
|
} for m in mindmaps])
|
||||||
|
|
||||||
|
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def api_get_user_mindmap_detail(mindmap_id):
|
||||||
|
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||||
|
# Bestehende Logik von get_user_mindmap kann hier wiederverwendet oder angepasst werden
|
||||||
|
# Für eine einfache Detailansicht:
|
||||||
|
return jsonify({
|
||||||
|
'id': mindmap.id,
|
||||||
|
'name': mindmap.name,
|
||||||
|
'description': mindmap.description,
|
||||||
|
'is_private': mindmap.is_private,
|
||||||
|
'user_id': mindmap.user_id,
|
||||||
|
'created_at': mindmap.created_at.isoformat(),
|
||||||
|
'last_modified': mindmap.last_modified.isoformat(),
|
||||||
|
# Hier könnten auch Knoten und Notizen hinzugefügt werden, ähnlich wie in get_user_mindmap
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def api_update_user_mindmap(mindmap_id):
|
||||||
|
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
mindmap.name = data.get('name', mindmap.name)
|
||||||
|
mindmap.description = data.get('description', mindmap.description)
|
||||||
|
mindmap.is_private = data.get('is_private', mindmap.is_private)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({
|
||||||
|
'id': mindmap.id,
|
||||||
|
'name': mindmap.name,
|
||||||
|
'description': mindmap.description,
|
||||||
|
'is_private': mindmap.is_private,
|
||||||
|
'last_modified': mindmap.last_modified.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/mindmaps/<int:mindmap_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def api_delete_user_mindmap(mindmap_id):
|
||||||
|
mindmap = UserMindmap.query.filter_by(id=mindmap_id, user_id=current_user.id).first_or_404()
|
||||||
|
db.session.delete(mindmap)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Mindmap erfolgreich gelöscht'}), 200
|
||||||
|
|
||||||
|
# API-Endpunkte für Mindmap-Daten (öffentlich und benutzerspezifisch)
|
||||||
@app.route('/api/mindmap/public')
|
@app.route('/api/mindmap/public')
|
||||||
def get_public_mindmap():
|
def get_public_mindmap():
|
||||||
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
"""Liefert die Standard-Mindmap-Struktur basierend auf Kategorien."""
|
||||||
@@ -1028,9 +1081,11 @@ def add_node_to_mindmap(mindmap_id):
|
|||||||
return jsonify({'error': 'Nicht autorisiert'}), 403
|
return jsonify({'error': 'Nicht autorisiert'}), 403
|
||||||
|
|
||||||
node_id = data.get('node_id')
|
node_id = data.get('node_id')
|
||||||
|
parent_id = data.get('parent_id') # Optional: Elternknoten für die Verknüpfung
|
||||||
x_pos = data.get('x', 0)
|
x_pos = data.get('x', 0)
|
||||||
y_pos = data.get('y', 0)
|
y_pos = data.get('y', 0)
|
||||||
|
|
||||||
|
# Knoten abrufen
|
||||||
node = MindMapNode.query.get_or_404(node_id)
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
|
||||||
# Prüfen, ob der Knoten bereits in der Mindmap existiert
|
# Prüfen, ob der Knoten bereits in der Mindmap existiert
|
||||||
@@ -1053,6 +1108,23 @@ def add_node_to_mindmap(mindmap_id):
|
|||||||
)
|
)
|
||||||
db.session.add(user_node)
|
db.session.add(user_node)
|
||||||
|
|
||||||
|
# Wenn ein Elternknoten angegeben wurde, Verbindung erstellen
|
||||||
|
if parent_id:
|
||||||
|
# Existenz des Elternknotens in der Mindmap prüfen
|
||||||
|
parent_node = MindMapNode.query.get(parent_id)
|
||||||
|
parent_user_node = UserMindmapNode.query.filter_by(
|
||||||
|
user_mindmap_id=mindmap_id,
|
||||||
|
node_id=parent_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if parent_node and parent_user_node:
|
||||||
|
# Beziehung zwischen den Knoten erstellen
|
||||||
|
try:
|
||||||
|
parent_node.children.append(node)
|
||||||
|
db.session.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warnung: Fehler beim Erstellen der Beziehung: {e}")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1082,6 +1154,24 @@ def remove_node_from_mindmap(mindmap_id, node_id):
|
|||||||
|
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/node/<int:node_id>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_node_info(node_id):
|
||||||
|
"""Liefert Detailinformationen zu einem einzelnen Knoten"""
|
||||||
|
try:
|
||||||
|
# Knoten abrufen
|
||||||
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'id': node.id,
|
||||||
|
'name': node.name,
|
||||||
|
'description': node.description or '',
|
||||||
|
'color_code': node.color_code or '#9F7AEA'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler in get_node_info: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/mindmap/<int:mindmap_id>/update_node_position', methods=['POST'])
|
@app.route('/api/mindmap/<int:mindmap_id>/update_node_position', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_node_position(mindmap_id):
|
def update_node_position(mindmap_id):
|
||||||
@@ -1918,7 +2008,7 @@ def admin_update_database():
|
|||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
import update_db
|
import utils.update_db as update_db
|
||||||
update_success = update_db.update_user_table()
|
update_success = update_db.update_user_table()
|
||||||
if update_success:
|
if update_success:
|
||||||
message = "Die Datenbank wurde erfolgreich aktualisiert."
|
message = "Die Datenbank wurde erfolgreich aktualisiert."
|
||||||
@@ -1936,6 +2026,7 @@ def admin_update_database():
|
|||||||
def get_mindmap_node(node_id):
|
def get_mindmap_node(node_id):
|
||||||
"""Liefert die Mindmap-Daten für einen bestimmten Knoten und seine Subthemen."""
|
"""Liefert die Mindmap-Daten für einen bestimmten Knoten und seine Subthemen."""
|
||||||
try:
|
try:
|
||||||
|
# Erkennen besonderer Knotennamen
|
||||||
if node_id == 'root':
|
if node_id == 'root':
|
||||||
# Hauptknoten (Wissen) abrufen
|
# Hauptknoten (Wissen) abrufen
|
||||||
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
wissen_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
@@ -1951,11 +2042,39 @@ def get_mindmap_node(node_id):
|
|||||||
|
|
||||||
# Alle direkten Kinder des Wissen-Knotens holen
|
# Alle direkten Kinder des Wissen-Knotens holen
|
||||||
nodes = wissen_node.children.all()
|
nodes = wissen_node.children.all()
|
||||||
else:
|
parent_node = wissen_node
|
||||||
|
elif node_id.isdigit():
|
||||||
# Bestimmten Knoten und seine Kinder abrufen
|
# Bestimmten Knoten und seine Kinder abrufen
|
||||||
parent_node = MindMapNode.query.get_or_404(node_id)
|
parent_node = MindMapNode.query.get_or_404(int(node_id))
|
||||||
|
nodes = parent_node.children.all()
|
||||||
|
else:
|
||||||
|
# Versuche, einen Knoten mit diesem Namen zu finden
|
||||||
|
parent_node = MindMapNode.query.filter_by(name=node_id.capitalize()).first()
|
||||||
|
|
||||||
|
if not parent_node:
|
||||||
|
# Versuche, eine Kategorie mit diesem Namen zu finden
|
||||||
|
category = Category.query.filter(func.lower(Category.name) == func.lower(node_id)).first()
|
||||||
|
if category:
|
||||||
|
# Finde oder erstelle einen Knoten für diese Kategorie
|
||||||
|
parent_node = MindMapNode.query.filter_by(category_id=category.id).first()
|
||||||
|
if not parent_node:
|
||||||
|
parent_node = MindMapNode(
|
||||||
|
name=category.name,
|
||||||
|
description=category.description,
|
||||||
|
color_code=category.color_code,
|
||||||
|
category_id=category.id,
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
db.session.add(parent_node)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if not parent_node:
|
||||||
|
# Fallback zum Wissen-Knoten
|
||||||
|
parent_node = MindMapNode.query.filter_by(name="Wissen").first()
|
||||||
|
if not parent_node:
|
||||||
|
return jsonify({'error': 'Knoten nicht gefunden'}), 404
|
||||||
|
|
||||||
nodes = parent_node.children.all()
|
nodes = parent_node.children.all()
|
||||||
wissen_node = parent_node
|
|
||||||
|
|
||||||
# Ergebnisdaten vorbereiten
|
# Ergebnisdaten vorbereiten
|
||||||
nodes_data = []
|
nodes_data = []
|
||||||
@@ -1963,10 +2082,10 @@ def get_mindmap_node(node_id):
|
|||||||
|
|
||||||
# Hauptknoten hinzufügen
|
# Hauptknoten hinzufügen
|
||||||
nodes_data.append({
|
nodes_data.append({
|
||||||
'id': wissen_node.id,
|
'id': parent_node.id,
|
||||||
'name': wissen_node.name,
|
'name': parent_node.name,
|
||||||
'description': wissen_node.description or '',
|
'description': parent_node.description or '',
|
||||||
'color_code': wissen_node.color_code or '#4299E1',
|
'color_code': parent_node.color_code or '#4299E1',
|
||||||
'is_center': True,
|
'is_center': True,
|
||||||
'has_children': len(nodes) > 0
|
'has_children': len(nodes) > 0
|
||||||
})
|
})
|
||||||
@@ -1984,7 +2103,7 @@ def get_mindmap_node(node_id):
|
|||||||
|
|
||||||
# Verbindung zum Elternknoten hinzufügen
|
# Verbindung zum Elternknoten hinzufügen
|
||||||
edges_data.append({
|
edges_data.append({
|
||||||
'source_id': wissen_node.id,
|
'source_id': parent_node.id,
|
||||||
'target_id': node.id,
|
'target_id': node.id,
|
||||||
'strength': 0.8
|
'strength': 0.8
|
||||||
})
|
})
|
||||||
@@ -1996,7 +2115,214 @@ def get_mindmap_node(node_id):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler beim Abrufen der Mindmap-Daten: {str(e)}")
|
print(f"Fehler beim Abrufen der Mindmap-Daten: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Mindmap-Daten konnten nicht geladen werden'
|
'error': 'Mindmap-Daten konnten nicht geladen werden'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
# API-Endpunkte für Notizen und Layout-Speicherung
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_node_notes(node_id):
|
||||||
|
"""Ruft die Notizen für einen Mindmap-Knoten ab"""
|
||||||
|
try:
|
||||||
|
# Prüfen, ob der Knoten existiert
|
||||||
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
|
||||||
|
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
|
||||||
|
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
|
||||||
|
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
|
||||||
|
).filter(UserMindmap.user_id == current_user.id).first()
|
||||||
|
|
||||||
|
if not user_node and not current_user.is_admin:
|
||||||
|
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
|
||||||
|
|
||||||
|
# Prüfen, ob eine Notiz für diesen Knoten existiert
|
||||||
|
note = MindmapNote.query.filter_by(
|
||||||
|
user_id=current_user.id,
|
||||||
|
node_id=node_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if note:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'notes': note.content,
|
||||||
|
'color_code': note.color_code
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'notes': '',
|
||||||
|
'color_code': '#FFF59D' # Standard-Gelb
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler in get_node_notes: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/node/<int:node_id>/notes', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_node_notes(node_id):
|
||||||
|
"""Aktualisiert die Notizen für einen Mindmap-Knoten"""
|
||||||
|
try:
|
||||||
|
# Prüfen, ob der Knoten existiert
|
||||||
|
node = MindMapNode.query.get_or_404(node_id)
|
||||||
|
|
||||||
|
# Prüfen, ob der Knoten in einer Mindmap des Benutzers ist
|
||||||
|
user_node = UserMindmapNode.query.filter_by(node_id=node_id).join(
|
||||||
|
UserMindmap, UserMindmapNode.user_mindmap_id == UserMindmap.id
|
||||||
|
).filter(UserMindmap.user_id == current_user.id).first()
|
||||||
|
|
||||||
|
if not user_node and not current_user.is_admin:
|
||||||
|
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
|
||||||
|
|
||||||
|
# Daten aus dem Request abrufen
|
||||||
|
data = request.json
|
||||||
|
notes_content = data.get('notes', '')
|
||||||
|
color_code = data.get('color_code', '#FFF59D') # Standard-Gelb
|
||||||
|
|
||||||
|
# Prüfen, ob bereits eine Notiz für diesen Knoten existiert
|
||||||
|
note = MindmapNote.query.filter_by(
|
||||||
|
user_id=current_user.id,
|
||||||
|
node_id=node_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if note:
|
||||||
|
# Vorhandene Notiz aktualisieren
|
||||||
|
note.content = notes_content
|
||||||
|
note.color_code = color_code
|
||||||
|
else:
|
||||||
|
# Neue Notiz erstellen - hier brauchen wir die Mindmap-ID
|
||||||
|
mindmap_id = user_node.user_mindmap_id
|
||||||
|
note = MindmapNote(
|
||||||
|
user_id=current_user.id,
|
||||||
|
mindmap_id=mindmap_id,
|
||||||
|
node_id=node_id,
|
||||||
|
content=notes_content,
|
||||||
|
color_code=color_code
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Notizen erfolgreich gespeichert'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler in update_node_notes: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/mindmap/<int:mindmap_id>/layout', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def save_mindmap_layout(mindmap_id):
|
||||||
|
"""Speichert das Layout (Positionen der Knoten) einer Mindmap"""
|
||||||
|
try:
|
||||||
|
# Prüfe, ob die Mindmap dem Benutzer gehört
|
||||||
|
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||||
|
if mindmap.user_id != current_user.id and not current_user.is_admin:
|
||||||
|
return jsonify({'success': False, 'message': 'Keine Berechtigung'}), 403
|
||||||
|
|
||||||
|
# Daten aus dem Request abrufen
|
||||||
|
data = request.json
|
||||||
|
nodes_data = data.get('nodes', [])
|
||||||
|
|
||||||
|
# Positionen aller Knoten aktualisieren
|
||||||
|
for node_data in nodes_data:
|
||||||
|
node_id = node_data.get('id')
|
||||||
|
x_pos = node_data.get('x')
|
||||||
|
y_pos = node_data.get('y')
|
||||||
|
|
||||||
|
if node_id and x_pos is not None and y_pos is not None:
|
||||||
|
# UserMindmapNode-Eintrag aktualisieren
|
||||||
|
user_node = UserMindmapNode.query.filter_by(
|
||||||
|
user_mindmap_id=mindmap_id,
|
||||||
|
node_id=node_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if user_node:
|
||||||
|
user_node.x_position = x_pos
|
||||||
|
user_node.y_position = y_pos
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Layout erfolgreich gespeichert'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Fehler in save_mindmap_layout: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/public-mindmap', methods=['GET'])
|
||||||
|
def get_public_mindmap_nodes():
|
||||||
|
"""Liefert die öffentliche Mindmap für die Knotenübernahme"""
|
||||||
|
try:
|
||||||
|
# Alle öffentlichen Knoten abrufen
|
||||||
|
public_nodes = MindMapNode.query.filter_by(is_public=True).all()
|
||||||
|
|
||||||
|
# Knoten formatieren
|
||||||
|
nodes = []
|
||||||
|
for node in public_nodes:
|
||||||
|
nodes.append({
|
||||||
|
'id': node.id,
|
||||||
|
'name': node.name,
|
||||||
|
'description': node.description or '',
|
||||||
|
'color_code': node.color_code or '#9F7AEA'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Alle Beziehungen zwischen Knoten abrufen
|
||||||
|
edges = []
|
||||||
|
for node in public_nodes:
|
||||||
|
for child in node.children:
|
||||||
|
if child.is_public:
|
||||||
|
edges.append({
|
||||||
|
'source': node.id,
|
||||||
|
'target': child.id
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'nodes': nodes,
|
||||||
|
'edges': edges
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler in get_public_mindmap_nodes: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
|
||||||
|
def initialize_app():
|
||||||
|
"""Initialisierung der Anwendung"""
|
||||||
|
print("Initialisierung der Anwendung...")
|
||||||
|
with app.app_context():
|
||||||
|
# Prüfen, ob die Datenbank existiert und initialisiert ist
|
||||||
|
try:
|
||||||
|
# Prüfen, ob Tabellen existieren
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Prüfen, ob Stammdaten vorhanden sind
|
||||||
|
if User.query.count() == 0:
|
||||||
|
print("Erstelle Standardbenutzer...")
|
||||||
|
create_default_users()
|
||||||
|
|
||||||
|
# Wir nutzen die initialize_database Funktion für Kategorien und Mindmap
|
||||||
|
# Diese Funktionalität ist bereits dort implementiert
|
||||||
|
initialize_database()
|
||||||
|
|
||||||
|
print("Datenbank wurde erfolgreich initialisiert.")
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Fehler bei der Datenbankinitialisierung: {e}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
# Moderne Methode für die Datenbankinitialisierung in Flask 2.2+
|
||||||
|
with app.app_context():
|
||||||
|
initialize_app()
|
||||||
Binary file not shown.
BIN
systades.db
BIN
systades.db
Binary file not shown.
@@ -231,7 +231,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-body">
|
<div class="form-body">
|
||||||
<form action="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" method="POST">
|
<form id="edit-mindmap-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name" class="form-label">Name der Mindmap</label>
|
<label for="name" class="form-label">Name der Mindmap</label>
|
||||||
<input type="text" id="name" name="name" class="form-input input-animation" required
|
<input type="text" id="name" name="name" class="form-input input-animation" required
|
||||||
@@ -253,11 +253,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between mt-6">
|
<div class="flex justify-between mt-6">
|
||||||
<a href="{{ url_for('mindmap', mindmap_id=mindmap.id) }}" class="btn-cancel">
|
<a href="{{ url_for('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
|
||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left"></i>
|
||||||
Zurück
|
Zurück
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn-submit">
|
<button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
|
||||||
<i class="fas fa-save"></i>
|
<i class="fas fa-save"></i>
|
||||||
Änderungen speichern
|
Änderungen speichern
|
||||||
</button>
|
</button>
|
||||||
@@ -322,13 +322,58 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Formular-Absenden-Animation
|
// Formular-Absenden-Logik für Metadaten
|
||||||
const form = document.querySelector('form');
|
const editMindmapForm = document.getElementById('edit-mindmap-form');
|
||||||
form.addEventListener('submit', function(e) {
|
const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
|
||||||
const submitBtn = this.querySelector('.btn-submit');
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
if (saveDetailsBtn && editMindmapForm) {
|
||||||
submitBtn.disabled = true;
|
saveDetailsBtn.addEventListener('click', async function(event) {
|
||||||
});
|
event.preventDefault();
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const descriptionInput = document.getElementById('description');
|
||||||
|
const isPrivateInput = document.getElementById('is_private');
|
||||||
|
|
||||||
|
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: nameInput.value,
|
||||||
|
description: descriptionInput.value,
|
||||||
|
is_private: isPrivateInput.checked
|
||||||
|
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
|
||||||
|
};
|
||||||
|
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
|
||||||
|
saveDetailsBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
showStatus('Metadaten erfolgreich gespeichert!', false);
|
||||||
|
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
|
||||||
|
// window.location.href = "{{ url_for('my_account') }}";
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Fehler beim Speichern der Metadaten:', errorData);
|
||||||
|
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler:', error);
|
||||||
|
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
|
||||||
|
} finally {
|
||||||
|
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
|
||||||
|
saveDetailsBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Mindmap initialisieren
|
// Mindmap initialisieren
|
||||||
const mindmap = new MindMap.Visualization('cy', {
|
const mindmap = new MindMap.Visualization('cy', {
|
||||||
@@ -337,56 +382,112 @@
|
|||||||
onNodeClick: function(nodeData) {
|
onNodeClick: function(nodeData) {
|
||||||
console.log("Knoten ausgewählt:", nodeData);
|
console.log("Knoten ausgewählt:", nodeData);
|
||||||
},
|
},
|
||||||
onChange: function(data) {
|
onChange: function(dataFromCytoscape) {
|
||||||
// Automatisches Speichern bei Änderungen
|
// Automatisches Speichern bei Änderungen der Mindmap-Struktur
|
||||||
fetch('/api/mindmap/{{ mindmap.id }}/update', {
|
// Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
|
||||||
method: 'POST',
|
// Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
|
||||||
headers: {
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': '{{ csrf_token() }}'
|
// Debounce-Funktion, um API-Aufrufe zu limitieren
|
||||||
},
|
let debounceTimer;
|
||||||
body: JSON.stringify(data)
|
const debounceSaveStructure = (currentMindmapData) => {
|
||||||
}).then(response => {
|
clearTimeout(debounceTimer);
|
||||||
if (!response.ok) {
|
debounceTimer = setTimeout(() => {
|
||||||
throw new Error('Netzwerkfehler beim Speichern');
|
// Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
|
||||||
}
|
// das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
|
||||||
console.log('Änderungen gespeichert');
|
const payload = {
|
||||||
}).catch(error => {
|
data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
|
||||||
console.error('Fehler beim Speichern:', error);
|
};
|
||||||
alert('Fehler beim Speichern der Änderungen');
|
|
||||||
});
|
// showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
|
||||||
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
|
||||||
|
method: 'PUT', // Methode zu PUT geändert
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
|
||||||
|
}).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur:', err);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
|
||||||
|
});
|
||||||
|
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
|
||||||
|
return; // Verhindere weitere Verarbeitung bei Fehler
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}).then(responseData => {
|
||||||
|
if (responseData) { // Nur wenn response.ok war
|
||||||
|
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
|
||||||
|
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
|
||||||
|
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
|
||||||
|
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
|
||||||
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
|
||||||
|
};
|
||||||
|
|
||||||
|
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Formularfelder mit Mindmap verbinden
|
// Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
|
||||||
const nameInput = document.getElementById('name');
|
// da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
|
||||||
const descriptionInput = document.getElementById('description');
|
// die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
|
||||||
|
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
|
||||||
// Aktualisiere Mindmap wenn sich die Eingaben ändern
|
// const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
|
||||||
nameInput.addEventListener('input', function() {
|
// nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
|
||||||
if (mindmap.cy) {
|
|
||||||
const rootNode = mindmap.cy.$('#root');
|
|
||||||
if (rootNode.length > 0) {
|
|
||||||
rootNode.data('name', this.value || 'Mindmap');
|
|
||||||
mindmap.saveToServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialisiere die Mindmap mit existierenden Daten
|
// Initialisiere die Mindmap mit existierenden Daten
|
||||||
mindmap.initialize().then(() => {
|
mindmap.initialize().then(() => {
|
||||||
console.log("Mindmap-Editor initialisiert");
|
console.log("Mindmap-Editor initialisiert");
|
||||||
|
const mindmapId = "{{ mindmap.id }}";
|
||||||
|
|
||||||
// Lade existierende Daten
|
// Lade existierende Daten für die Mindmap-Struktur
|
||||||
fetch('/api/mindmap/{{ mindmap.id }}/data')
|
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
|
||||||
.then(response => response.json())
|
method: 'GET',
|
||||||
.then(data => {
|
headers: {
|
||||||
mindmap.loadData(data);
|
'Accept': 'application/json'
|
||||||
console.log("Mindmap-Daten geladen");
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
response.json().then(err => {
|
||||||
|
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
|
||||||
|
}).catch(() => {
|
||||||
|
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
|
||||||
|
});
|
||||||
|
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(mindmapDataFromServer => {
|
||||||
|
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
|
||||||
|
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
|
||||||
|
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
|
||||||
|
if (mindmapDataFromServer && mindmapDataFromServer.data) {
|
||||||
|
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
|
||||||
|
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
|
||||||
|
showStatus("Mindmap geladen.", false);
|
||||||
|
} else {
|
||||||
|
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
|
||||||
|
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Fehler beim Laden der Mindmap-Daten:", error);
|
console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
|
||||||
alert("Fehler beim Laden der Mindmap");
|
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
|
||||||
|
showStatus("Laden der Struktur fehlgeschlagen.", true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error("Fehler bei der Initialisierung des Editors:", error);
|
console.error("Fehler bei der Initialisierung des Editors:", error);
|
||||||
@@ -411,8 +512,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Event-Listener für Speicherstatus
|
// Event-Listener für Speicherstatus
|
||||||
document.addEventListener('mindmapSaved', () => {
|
document.addEventListener('mindmapSaved', (event) => {
|
||||||
showStatus('Änderungen gespeichert');
|
const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
|
||||||
|
showStatus(message, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mindmapError', (event) => {
|
document.addEventListener('mindmapError', (event) => {
|
||||||
|
|||||||
@@ -85,6 +85,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Meine erstellten Mindmaps -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white flex items-center">
|
||||||
|
<i class="fas fa-brain mr-3 text-green-500"></i>
|
||||||
|
Meine erstellten Mindmaps
|
||||||
|
</h2>
|
||||||
|
<button id="create-mindmap-btn" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center">
|
||||||
|
<i class="fas fa-plus mr-2"></i> Neue Mindmap erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="user-mindmaps-container" class="space-y-4">
|
||||||
|
<!-- Hier werden die Mindmaps des Benutzers geladen -->
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Gemerkte Inhalte -->
|
<!-- Gemerkte Inhalte -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
<!-- Wissensbereiche -->
|
<!-- Wissensbereiche -->
|
||||||
@@ -123,6 +140,431 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal zum Erstellen einer neuen Mindmap -->
|
||||||
|
<div id="create-mindmap-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50">
|
||||||
|
<div class="relative mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Neue Mindmap erstellen</h3>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<form id="create-mindmap-form">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="mindmap-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Name</label>
|
||||||
|
<input type="text" name="name" id="mindmap-name" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="mindmap-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Beschreibung (optional)</label>
|
||||||
|
<textarea name="description" id="mindmap-description" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="items-center px-4 py-3">
|
||||||
|
<button id="submit-create-mindmap" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
<button id="cancel-create-mindmap" class="mt-2 px-4 py-2 bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-gray-200 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript für persönliche Mindmap und CRUD -->
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Modal-Logik
|
||||||
|
const createMindmapBtn = document.getElementById('create-mindmap-btn');
|
||||||
|
const createMindmapModal = document.getElementById('create-mindmap-modal');
|
||||||
|
const cancelCreateMindmapBtn = document.getElementById('cancel-create-mindmap');
|
||||||
|
const submitCreateMindmapBtn = document.getElementById('submit-create-mindmap');
|
||||||
|
const createMindmapForm = document.getElementById('create-mindmap-form');
|
||||||
|
|
||||||
|
if (createMindmapBtn) {
|
||||||
|
createMindmapBtn.addEventListener('click', () => {
|
||||||
|
createMindmapModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelCreateMindmapBtn) {
|
||||||
|
cancelCreateMindmapBtn.addEventListener('click', () => {
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schließen bei Klick außerhalb des Modals
|
||||||
|
if (createMindmapModal) {
|
||||||
|
createMindmapModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === createMindmapModal) {
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Anzeigen von Benachrichtigungen
|
||||||
|
function showNotification(message, type = 'success') {
|
||||||
|
const notificationArea = document.getElementById('notification-area') || createNotificationArea();
|
||||||
|
const notificationId = `notif-${Date.now()}`;
|
||||||
|
constbgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
|
||||||
|
|
||||||
|
const notificationElement = `
|
||||||
|
<div id="${notificationId}" class="p-4 mb-4 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
|
||||||
|
<span class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}:</span> ${message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById(notificationId);
|
||||||
|
if (el) {
|
||||||
|
el.classList.add('animate-fadeOut');
|
||||||
|
setTimeout(() => el.remove(), 500);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNotificationArea() {
|
||||||
|
const area = document.createElement('div');
|
||||||
|
area.id = 'notification-area';
|
||||||
|
area.className = 'fixed top-5 right-5 z-50 w-auto max-w-sm';
|
||||||
|
document.body.appendChild(area);
|
||||||
|
// Add some basic animation styles
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.animate-fadeIn { animation: fadeIn 0.5s ease-out; }
|
||||||
|
.animate-fadeOut { animation: fadeOut 0.5s ease-in forwards; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CRUD-Funktionen für UserMindmaps
|
||||||
|
const mindmapsContainer = document.getElementById('user-mindmaps-container');
|
||||||
|
|
||||||
|
async function fetchUserMindmaps() {
|
||||||
|
if (!mindmapsContainer) return;
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/mindmaps');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const mindmaps = await response.json();
|
||||||
|
renderMindmaps(mindmaps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Mindmaps:', error);
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-red-500">Fehler beim Laden der Mindmaps.</p>';
|
||||||
|
showNotification('Fehler beim Laden der Mindmaps.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMindmaps(mindmaps) {
|
||||||
|
if (!mindmapsContainer) return;
|
||||||
|
if (mindmaps.length === 0) {
|
||||||
|
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Du hast noch keine eigenen Mindmaps erstellt.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mindmapsContainer.innerHTML = ''; // Container leeren
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className = 'space-y-3';
|
||||||
|
|
||||||
|
mindmaps.forEach(mindmap => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all flex justify-between items-center';
|
||||||
|
|
||||||
|
const mindmapLink = document.createElement('a');
|
||||||
|
mindmapLink.href = `/user_mindmap/${mindmap.id}`;
|
||||||
|
mindmapLink.className = 'flex-grow';
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
const nameH3 = document.createElement('h3');
|
||||||
|
nameH3.className = 'font-semibold text-gray-900 dark:text-white';
|
||||||
|
nameH3.textContent = mindmap.name;
|
||||||
|
textDiv.appendChild(nameH3);
|
||||||
|
|
||||||
|
if (mindmap.description) {
|
||||||
|
const descP = document.createElement('p');
|
||||||
|
descP.className = 'text-sm text-gray-600 dark:text-gray-400';
|
||||||
|
descP.textContent = mindmap.description;
|
||||||
|
textDiv.appendChild(descP);
|
||||||
|
}
|
||||||
|
mindmapLink.appendChild(textDiv);
|
||||||
|
li.appendChild(mindmapLink);
|
||||||
|
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'flex space-x-2 ml-4';
|
||||||
|
|
||||||
|
const editButton = document.createElement('a');
|
||||||
|
editButton.href = `/edit_mindmap/${mindmap.id}`; // oder JavaScript-basiertes Editieren
|
||||||
|
editButton.className = 'px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm flex items-center';
|
||||||
|
editButton.innerHTML = '<i class="fas fa-edit mr-1"></i> Bearbeiten';
|
||||||
|
// Hier könnte auch ein Event-Listener für ein Modal zum Bearbeiten hinzugefügt werden
|
||||||
|
// editButton.addEventListener('click', (e) => { e.preventDefault(); openEditModal(mindmap); });
|
||||||
|
actionsDiv.appendChild(editButton);
|
||||||
|
|
||||||
|
const deleteButton = document.createElement('button');
|
||||||
|
deleteButton.className = 'px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm flex items-center delete-mindmap-btn';
|
||||||
|
deleteButton.innerHTML = '<i class="fas fa-trash mr-1"></i> Löschen';
|
||||||
|
deleteButton.dataset.mindmapId = mindmap.id;
|
||||||
|
actionsDiv.appendChild(deleteButton);
|
||||||
|
|
||||||
|
li.appendChild(actionsDiv);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
mindmapsContainer.appendChild(ul);
|
||||||
|
|
||||||
|
// Event Listener für Löschen-Buttons hinzufügen
|
||||||
|
document.querySelectorAll('.delete-mindmap-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', async (event) => {
|
||||||
|
const mindmapId = event.currentTarget.dataset.mindmapId;
|
||||||
|
if (confirm('Bist du sicher, dass du diese Mindmap löschen möchtest?')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
showNotification('Mindmap erfolgreich gelöscht.', 'success');
|
||||||
|
fetchUserMindmaps(); // Liste aktualisieren
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Löschen der Mindmap:', error);
|
||||||
|
showNotification(`Fehler beim Löschen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitCreateMindmapBtn) {
|
||||||
|
submitCreateMindmapBtn.addEventListener('click', async () => {
|
||||||
|
const name = document.getElementById('mindmap-name').value;
|
||||||
|
const description = document.getElementById('mindmap-description').value;
|
||||||
|
|
||||||
|
if (!name.trim()) {
|
||||||
|
showNotification('Der Name der Mindmap darf nicht leer sein.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/mindmaps', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, description, is_private: false }), // is_private standardmäßig auf false setzen
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const newMindmap = await response.json();
|
||||||
|
showNotification(`Mindmap "${newMindmap.name}" erfolgreich erstellt. Weiterleitung...`, 'success');
|
||||||
|
createMindmapModal.classList.add('hidden');
|
||||||
|
createMindmapForm.reset();
|
||||||
|
// fetchUserMindmaps(); // Liste wird auf der neuen Seite ohnehin neu geladen oder ist nicht direkt sichtbar.
|
||||||
|
// Weiterleitung zur Bearbeitungsseite der neuen Mindmap
|
||||||
|
window.location.href = `/edit_mindmap/${newMindmap.id}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen der Mindmap:', error);
|
||||||
|
showNotification(`Fehler beim Erstellen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiale Ladefunktion für Mindmaps
|
||||||
|
fetchUserMindmaps();
|
||||||
|
|
||||||
|
// Bestehendes Skript für Bookmarks etc.
|
||||||
|
// Lade gespeicherte Bookmarks aus dem LocalStorage
|
||||||
|
function loadBookmarkedNodes() {
|
||||||
|
try {
|
||||||
|
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||||
|
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkedNodeIds = loadBookmarkedNodes();
|
||||||
|
|
||||||
|
// Prüfe, ob es gemerkte Knoten gibt
|
||||||
|
if (bookmarkedNodeIds && bookmarkedNodeIds.length > 0) {
|
||||||
|
// Verstecke die Leer-Nachricht
|
||||||
|
const emptyMindmapMessage = document.getElementById('empty-mindmap-message');
|
||||||
|
if (emptyMindmapMessage) {
|
||||||
|
emptyMindmapMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere die persönliche Mindmap
|
||||||
|
const personalMindmapContainer = document.getElementById('personal-mindmap');
|
||||||
|
if (personalMindmapContainer && typeof MindMapVisualization !== 'undefined') {
|
||||||
|
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
|
||||||
|
width: personalMindmapContainer.clientWidth,
|
||||||
|
height: 400,
|
||||||
|
nodeRadius: 18,
|
||||||
|
selectedNodeRadius: 22,
|
||||||
|
linkDistance: 120,
|
||||||
|
chargeStrength: -800,
|
||||||
|
centerForce: 0.1,
|
||||||
|
tooltipEnabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lade Daten für die Mindmap
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (window.mindmapInstance) {
|
||||||
|
const nodes = window.mindmapInstance.nodes.filter(node =>
|
||||||
|
bookmarkedNodeIds.includes(node.id)
|
||||||
|
);
|
||||||
|
const links = window.mindmapInstance.links.filter(link =>
|
||||||
|
bookmarkedNodeIds.includes(link.source.id || link.source) &&
|
||||||
|
bookmarkedNodeIds.includes(link.target.id || link.target)
|
||||||
|
);
|
||||||
|
personalMindmap.nodes = nodes;
|
||||||
|
personalMindmap.links = links;
|
||||||
|
personalMindmap.isLoading = false;
|
||||||
|
personalMindmap.updateVisualization();
|
||||||
|
} else {
|
||||||
|
if (emptyMindmapMessage) emptyMindmapMessage.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
loadBookmarkedContent(bookmarkedNodeIds);
|
||||||
|
} else {
|
||||||
|
// Zeige Leerzustand an
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
if (areasContainer) {
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thoughtsContainer) {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Laden der gemerkten Inhalte (bleibt größtenteils gleich)
|
||||||
|
function loadBookmarkedContent(nodeIds) {
|
||||||
|
if (!nodeIds || nodeIds.length === 0) return;
|
||||||
|
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
|
||||||
|
|
||||||
|
if (areasContainer) areasContainer.innerHTML = '';
|
||||||
|
if (thoughtsContainer) thoughtsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const areaTemplates = [
|
||||||
|
{ name: 'Philosophie', description: 'Grundlagen philosophischen Denkens', count: 24 },
|
||||||
|
{ name: 'Wissenschaft', description: 'Wissenschaftliche Methoden und Erkenntnisse', count: 42 },
|
||||||
|
{ name: 'Technologie', description: 'Zukunftsweisende Technologien', count: 36 },
|
||||||
|
{ name: 'Kunst', description: 'Künstlerische Ausdrucksformen', count: 18 },
|
||||||
|
{ name: 'Psychologie', description: 'Menschliches Verhalten verstehen', count: 30 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const thoughtTemplates = [
|
||||||
|
{ title: 'Quantenphysik und Bewusstsein', author: 'Maria Schmidt', date: '12.04.2023' },
|
||||||
|
{ title: 'Ethik in der künstlichen Intelligenz', author: 'Thomas Weber', date: '23.02.2023' },
|
||||||
|
{ title: 'Die Rolle der Kunst in der Gesellschaft', author: 'Lena Müller', date: '05.06.2023' },
|
||||||
|
{ title: 'Nachhaltige Entwicklung im 21. Jahrhundert', author: 'Michael Bauer', date: '18.08.2023' },
|
||||||
|
{ title: 'Kognitive Verzerrungen im Alltag', author: 'Sophie Klein', date: '30.09.2023' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const areaCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (areasContainer && areaCount > 0) {
|
||||||
|
for (let i = 0; i < areaCount; i++) {
|
||||||
|
const area = areaTemplates[i];
|
||||||
|
const colorClass = colors[i % colors.length];
|
||||||
|
areasContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-bookmark text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${area.name}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">${area.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
${area.count} Einträge
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else if (areasContainer) {
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thoughtCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (thoughtsContainer && thoughtCount > 0) {
|
||||||
|
for (let i = 0; i < thoughtCount; i++) {
|
||||||
|
const thought = thoughtTemplates[i];
|
||||||
|
const colorClass = colors[(i + 2) % colors.length];
|
||||||
|
thoughtsContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-lightbulb text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${thought.title}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Von ${thought.author} • ${thought.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else if (thoughtsContainer) {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<!-- JavaScript für persönliche Mindmap -->
|
<!-- JavaScript für persönliche Mindmap -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|||||||
@@ -319,12 +319,22 @@
|
|||||||
<i class="fas fa-save"></i>
|
<i class="fas fa-save"></i>
|
||||||
<span>Layout speichern</span>
|
<span>Layout speichern</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="toggle-public-mindmap-btn" class="mindmap-action-btn">
|
||||||
|
<i class="fas fa-globe"></i>
|
||||||
|
<span>Öffentliche Mindmap</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mindmap Container mit Positionsindikator -->
|
<!-- Mindmap Container mit Positionsindikator -->
|
||||||
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
|
<div class="relative rounded-xl overflow-hidden border transition-all duration-300"
|
||||||
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
|
x-bind:class="darkMode ? 'border-gray-700/50' : 'border-gray-300/50'">
|
||||||
<div id="cy"></div>
|
<div id="cy" data-mindmap-id="{{ mindmap.id }}"></div>
|
||||||
|
|
||||||
|
<!-- Container für öffentliche Mindmap (zur Knotenübernahme) -->
|
||||||
|
<div id="public-mindmap-container" class="hidden mt-4 border-t border-gray-300 dark:border-gray-700 pt-4" style="height: 300px;">
|
||||||
|
<h3 class="text-lg font-semibold mb-2 text-center">Öffentliche Mindmap</h3>
|
||||||
|
<div id="public-cy" style="width: 100%; height: 250px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Informationsanzeige für ausgewählten Knoten -->
|
<!-- Informationsanzeige für ausgewählten Knoten -->
|
||||||
<div id="node-info-panel" class="node-info-panel p-4">
|
<div id="node-info-panel" class="node-info-panel p-4">
|
||||||
@@ -353,39 +363,600 @@
|
|||||||
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/cytoscape.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/mindmap-init.js') }}"></script>
|
||||||
<script nonce="{{ csp_nonce }}">
|
<script nonce="{{ csp_nonce }}">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
// Benutzer-Mindmap-ID für die API-Anfragen
|
const cyContainer = document.getElementById('cy');
|
||||||
const mindmapId = {{ mindmap.id }};
|
if (!cyContainer) {
|
||||||
|
console.error("Mindmap container #cy not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mindmapId = cyContainer.dataset.mindmapId;
|
||||||
|
const nodeInfoPanel = document.getElementById('node-info-panel');
|
||||||
|
const nodeDescription = document.getElementById('node-description');
|
||||||
|
const connectedNodesContainer = document.getElementById('connected-nodes');
|
||||||
|
const mindmapNameH1 = document.querySelector('h1.gradient-text');
|
||||||
|
const mindmapDescriptionP = document.querySelector('p.opacity-80.mt-1');
|
||||||
|
|
||||||
// Erstellt eine neue MindMap-Instanz für die Benutzer-Mindmap
|
// Funktion zum Anzeigen von Benachrichtigungen (vereinfacht)
|
||||||
window.userMindmap = new MindMap('#cy', {
|
function showUINotification(message, type = 'success') {
|
||||||
editable: true,
|
const notificationArea = document.getElementById('notification-area-usr') || createUINotificationArea();
|
||||||
isUserLoggedIn: true,
|
const notificationId = `notif-usr-${Date.now()}`;
|
||||||
isPublicMap: false,
|
const bgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
|
||||||
userMindmapId: mindmapId,
|
|
||||||
fitViewOnInit: true,
|
const notificationElement = `
|
||||||
callbacks: {
|
<div id="${notificationId}" class="p-3 mb-3 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
|
||||||
onLoad: function() {
|
<span>${message}</span>
|
||||||
console.log('Benutzerdefinierte Mindmap wurde geladen');
|
</div>
|
||||||
|
`;
|
||||||
|
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById(notificationId);
|
||||||
|
if (el) {
|
||||||
|
el.classList.add('animate-fadeOut');
|
||||||
|
setTimeout(() => el.remove(), 500);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUINotificationArea() {
|
||||||
|
const area = document.createElement('div');
|
||||||
|
area.id = 'notification-area-usr';
|
||||||
|
area.className = 'fixed top-20 right-5 z-[1001] w-auto max-w-xs'; // höhere z-index
|
||||||
|
document.body.appendChild(area);
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
|
||||||
|
.animate-fadeOut { animation: fadeOut 0.3s ease-in forwards; }
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
||||||
|
@keyframes fadeOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMindmapData(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/mindmaps/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
||||||
|
showUINotification('Fehler beim Laden der Mindmap-Daten.', 'error');
|
||||||
|
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Konnte Mindmap nicht laden.</p>';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Event-Listener für Notiz-Button
|
const mindmapData = await fetchMindmapData(mindmapId);
|
||||||
document.getElementById('add-note-btn').addEventListener('click', function() {
|
|
||||||
// Erstellt eine neue Notiz in der Mitte des Viewports
|
|
||||||
const position = window.userMindmap.cy.pan();
|
|
||||||
|
|
||||||
window.userMindmap.showAddNoteDialog({
|
if (mindmapData) {
|
||||||
x: position.x,
|
if(mindmapNameH1) mindmapNameH1.textContent = mindmapData.name;
|
||||||
y: position.y
|
if(mindmapDescriptionP) mindmapDescriptionP.textContent = mindmapData.description || "Keine Beschreibung vorhanden.";
|
||||||
|
|
||||||
|
// Cytoscape initialisieren
|
||||||
|
const cy = cytoscape({
|
||||||
|
container: cyContainer,
|
||||||
|
elements: mindmapData.elements || [], // Verwende 'elements' aus der API-Antwort
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-color': 'data(color)',
|
||||||
|
'label': 'data(label)',
|
||||||
|
'color': 'data(fontColor)',
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'font-size': 'data(fontSize)',
|
||||||
|
'width': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
|
||||||
|
'height': ele => ele.data('isCenter') ? 60 : (ele.data('size') || 40),
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#fff',
|
||||||
|
'shape': 'ellipse',
|
||||||
|
'text-outline-color': '#555',
|
||||||
|
'text-outline-width': 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': ele => ele.data('strength') ? ele.data('strength') * 1.5 : 2,
|
||||||
|
'line-color': ele => ele.data('color') || '#9dbaea',
|
||||||
|
'target-arrow-color': ele => ele.data('color') || '#9dbaea',
|
||||||
|
'target-arrow-shape': 'triangle',
|
||||||
|
'curve-style': 'bezier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node:selected',
|
||||||
|
style: {
|
||||||
|
'border-color': '#f59e42',
|
||||||
|
'border-width': 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
layout: {
|
||||||
|
name: 'cose',
|
||||||
|
idealEdgeLength: 100,
|
||||||
|
nodeOverlap: 20,
|
||||||
|
refresh: 20,
|
||||||
|
fit: true,
|
||||||
|
padding: 30,
|
||||||
|
randomize: false,
|
||||||
|
componentSpacing: 100,
|
||||||
|
nodeRepulsion: 400000,
|
||||||
|
edgeElasticity: 100,
|
||||||
|
nestingFactor: 5,
|
||||||
|
gravity: 80,
|
||||||
|
numIter: 1000,
|
||||||
|
initialTemp: 200,
|
||||||
|
coolingFactor: 0.95,
|
||||||
|
minTemp: 1.0
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
window.cyInstance = cy; // Für globalen Zugriff falls nötig
|
||||||
|
|
||||||
// Event-Listener für Layout-Speichern-Button
|
cy.on('tap', 'node', function(evt){
|
||||||
document.getElementById('save-layout-btn').addEventListener('click', function() {
|
const node = evt.target;
|
||||||
window.userMindmap.saveLayout();
|
if (nodeDescription) nodeDescription.textContent = node.data('description') || 'Keine Beschreibung für diesen Knoten.';
|
||||||
});
|
|
||||||
});
|
if (connectedNodesContainer) {
|
||||||
|
connectedNodesContainer.innerHTML = '';
|
||||||
|
const connected = node.connectedEdges().otherNodes();
|
||||||
|
if (connected.length > 0) {
|
||||||
|
connected.forEach(cn => {
|
||||||
|
const link = document.createElement('span');
|
||||||
|
link.className = 'node-link';
|
||||||
|
link.textContent = cn.data('label');
|
||||||
|
link.style.backgroundColor = cn.data('color') || '#60a5fa';
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
cy.center(cn);
|
||||||
|
cn.select();
|
||||||
|
// Info Panel für den geklickten verbundenen Knoten aktualisieren
|
||||||
|
if (nodeDescription) nodeDescription.textContent = cn.data('description') || 'Keine Beschreibung für diesen Knoten.';
|
||||||
|
// Rekursiv verbundene Knoten des neu ausgewählten Knotens anzeigen (optional)
|
||||||
|
});
|
||||||
|
connectedNodesContainer.appendChild(link);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
connectedNodesContainer.innerHTML = '<p class="opacity-70 text-sm">Keine direkten Verbindungen.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeInfoPanel) nodeInfoPanel.classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.on('tap', function(evt){
|
||||||
|
if(evt.target === cy){ // Klick auf Hintergrund
|
||||||
|
if (nodeInfoPanel) nodeInfoPanel.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toolbar-Buttons
|
||||||
|
document.getElementById('fit-btn')?.addEventListener('click', () => cy.fit(null, 50));
|
||||||
|
document.getElementById('reset-btn')?.addEventListener('click', () => cy.layout({name: 'cose', animate:true}).run());
|
||||||
|
|
||||||
|
let labelsVisible = true;
|
||||||
|
document.getElementById('toggle-labels-btn')?.addEventListener('click', () => {
|
||||||
|
labelsVisible = !labelsVisible;
|
||||||
|
cy.style().selector('node').style({'text-opacity': labelsVisible ? 1 : 0}).update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notizfunktion implementieren
|
||||||
|
document.getElementById('add-note-btn').addEventListener('click', function() {
|
||||||
|
const selectedNodes = cy.$('node:selected');
|
||||||
|
if (selectedNodes.length === 0) {
|
||||||
|
showUINotification('Bitte wählen Sie einen Knoten aus, um eine Notiz hinzuzufügen.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = selectedNodes[0].id();
|
||||||
|
|
||||||
|
// Notizen für den ausgewählten Knoten laden
|
||||||
|
fetch(`/api/mindmap/node/${nodeId}/notes`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Notiz-Dialog anzeigen
|
||||||
|
showNoteDialog(nodeId, data.notes || '', data.color_code || '#FFF59D');
|
||||||
|
} else {
|
||||||
|
showUINotification('Fehler beim Laden der Notizen: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Notizen:', error);
|
||||||
|
showUINotification('Fehler beim Laden der Notizen', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layout speichern implementieren
|
||||||
|
document.getElementById('save-layout-btn').addEventListener('click', function() {
|
||||||
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||||
|
const nodes = [];
|
||||||
|
|
||||||
|
// Sammle die Positionen aller Knoten
|
||||||
|
cy.nodes().forEach(node => {
|
||||||
|
nodes.push({
|
||||||
|
id: node.id(),
|
||||||
|
x: node.position().x,
|
||||||
|
y: node.position().y
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layout speichern
|
||||||
|
fetch(`/api/mindmap/${mindmapId}/layout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ nodes: nodes })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showUINotification('Layout erfolgreich gespeichert', 'success');
|
||||||
|
} else {
|
||||||
|
showUINotification('Fehler beim Speichern des Layouts: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern des Layouts:', error);
|
||||||
|
showUINotification('Fehler beim Speichern des Layouts', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Öffentliche Mindmap anzeigen/verbergen
|
||||||
|
document.getElementById('toggle-public-mindmap-btn').addEventListener('click', function() {
|
||||||
|
const container = document.getElementById('public-mindmap-container');
|
||||||
|
if (container.classList.contains('hidden')) {
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
this.querySelector('span').textContent = 'Öffentliche Mindmap verbergen';
|
||||||
|
loadPublicMindmap();
|
||||||
|
} else {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
this.querySelector('span').textContent = 'Öffentliche Mindmap';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Anzeigen des Notiz-Dialogs
|
||||||
|
function showNoteDialog(nodeId, noteContent, colorCode) {
|
||||||
|
// Dialog erstellen
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||||
|
overlay.id = 'note-dialog-overlay';
|
||||||
|
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h3 class="text-xl font-bold mb-4">Notizen bearbeiten</h3>
|
||||||
|
<textarea id="note-content" class="w-full h-40 p-2 border rounded-lg mb-4 dark:bg-gray-700 dark:border-gray-600 dark:text-white">${noteContent}</textarea>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<label class="mr-2">Farbe:</label>
|
||||||
|
<input type="color" id="note-color" value="${colorCode}" class="cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button id="cancel-note" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||||
|
<button id="save-note" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Event-Listener für Buttons
|
||||||
|
document.getElementById('cancel-note').addEventListener('click', function() {
|
||||||
|
document.getElementById('note-dialog-overlay').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('save-note').addEventListener('click', function() {
|
||||||
|
const noteContent = document.getElementById('note-content').value;
|
||||||
|
const colorCode = document.getElementById('note-color').value;
|
||||||
|
|
||||||
|
// Notizen speichern
|
||||||
|
fetch(`/api/mindmap/node/${nodeId}/notes`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
notes: noteContent,
|
||||||
|
color_code: colorCode
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showUINotification('Notizen erfolgreich gespeichert', 'success');
|
||||||
|
document.getElementById('note-dialog-overlay').remove();
|
||||||
|
|
||||||
|
// Knoten visuell markieren, der eine Notiz hat
|
||||||
|
if (noteContent.trim() !== '') {
|
||||||
|
cy.$id(nodeId).addClass('has-note');
|
||||||
|
|
||||||
|
// Stil für Knoten mit Notizen hinzufügen, falls noch nicht vorhanden
|
||||||
|
if (!cy.style().selector('node.has-note').style('border-color')) {
|
||||||
|
cy.style()
|
||||||
|
.selector('node.has-note')
|
||||||
|
.style({
|
||||||
|
'border-color': colorCode,
|
||||||
|
'border-width': 3
|
||||||
|
})
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cy.$id(nodeId).removeClass('has-note');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showUINotification('Fehler beim Speichern der Notizen: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern der Notizen:', error);
|
||||||
|
showUINotification('Fehler beim Speichern der Notizen', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Laden der öffentlichen Mindmap
|
||||||
|
function loadPublicMindmap() {
|
||||||
|
const container = document.getElementById('public-cy');
|
||||||
|
|
||||||
|
// Lade-Anzeige
|
||||||
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p>Lade öffentliche Mindmap...</p></div>';
|
||||||
|
|
||||||
|
// Daten abrufen
|
||||||
|
fetch('/api/public-mindmap')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Initialisiere Cytoscape für die öffentliche Mindmap
|
||||||
|
const publicCy = cytoscape({
|
||||||
|
container: container,
|
||||||
|
elements: [],
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-color': 'data(color_code)',
|
||||||
|
'label': 'data(name)',
|
||||||
|
'color': '#FFFFFF',
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'width': '30px',
|
||||||
|
'height': '30px',
|
||||||
|
'text-wrap': 'wrap',
|
||||||
|
'text-max-width': '80px',
|
||||||
|
'font-size': '10px',
|
||||||
|
'border-width': '2px',
|
||||||
|
'border-color': '#FFFFFF',
|
||||||
|
'text-outline-color': '#555',
|
||||||
|
'text-outline-width': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': 1,
|
||||||
|
'line-color': '#9CA3AF',
|
||||||
|
'target-arrow-color': '#9CA3AF',
|
||||||
|
'target-arrow-shape': 'triangle',
|
||||||
|
'curve-style': 'bezier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node:selected',
|
||||||
|
style: {
|
||||||
|
'border-color': '#F59E0B',
|
||||||
|
'border-width': '3px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
layout: {
|
||||||
|
name: 'cose',
|
||||||
|
padding: 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Elemente zur öffentlichen Mindmap hinzufügen
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
// Knoten hinzufügen
|
||||||
|
data.nodes.forEach(node => {
|
||||||
|
elements.push({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: 'pub_' + node.id,
|
||||||
|
originalId: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description,
|
||||||
|
color_code: node.color_code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kanten hinzufügen
|
||||||
|
data.edges.forEach(edge => {
|
||||||
|
elements.push({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
source: 'pub_' + edge.source,
|
||||||
|
target: 'pub_' + edge.target
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
publicCy.add(elements);
|
||||||
|
publicCy.layout({ name: 'cose' }).run();
|
||||||
|
|
||||||
|
// Event-Listener für Knoten-Klicks in der öffentlichen Mindmap
|
||||||
|
publicCy.on('tap', 'node', function(evt) {
|
||||||
|
const node = evt.target;
|
||||||
|
const nodeData = node.data();
|
||||||
|
|
||||||
|
// Dialog zum Hinzufügen des Knotens anzeigen
|
||||||
|
showAddNodeDialog(nodeData.originalId, nodeData.name, nodeData.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
|
||||||
|
showUINotification('Fehler beim Laden der öffentlichen Mindmap: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der öffentlichen Mindmap:', error);
|
||||||
|
container.innerHTML = '<div class="flex justify-center items-center h-full"><p class="text-red-500">Fehler beim Laden der öffentlichen Mindmap</p></div>';
|
||||||
|
showUINotification('Fehler beim Laden der öffentlichen Mindmap', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Anzeigen des Dialogs zum Hinzufügen eines Knotens
|
||||||
|
function showAddNodeDialog(nodeId, nodeName, nodeDescription) {
|
||||||
|
// Dialog erstellen
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||||
|
overlay.id = 'add-node-dialog-overlay';
|
||||||
|
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h3 class="text-xl font-bold mb-4">Knoten zu Ihrer Mindmap hinzufügen</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="font-semibold">${nodeName}</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">${nodeDescription || 'Keine Beschreibung'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2">Mit Knoten verbinden (optional):</label>
|
||||||
|
<select id="parent-node-select" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||||
|
<option value="">Nicht verbinden</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button id="cancel-add-node" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||||
|
<button id="confirm-add-node" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Fülle den Parent-Node-Selector mit vorhandenen Knoten
|
||||||
|
const parentSelect = document.getElementById('parent-node-select');
|
||||||
|
cy.nodes().forEach(node => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = node.id();
|
||||||
|
option.textContent = node.data('label');
|
||||||
|
parentSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event-Listener für Buttons
|
||||||
|
document.getElementById('cancel-add-node').addEventListener('click', function() {
|
||||||
|
document.getElementById('add-node-dialog-overlay').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('confirm-add-node').addEventListener('click', function() {
|
||||||
|
const parentNodeId = document.getElementById('parent-node-select').value;
|
||||||
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||||
|
|
||||||
|
// Generiere Startposition basierend auf Parent oder Zufallsposition
|
||||||
|
let position = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
if (parentNodeId) {
|
||||||
|
const parentNode = cy.$id(parentNodeId);
|
||||||
|
if (parentNode) {
|
||||||
|
// Positioniere in der Nähe des Elternknotens
|
||||||
|
position = {
|
||||||
|
x: parentNode.position('x') + Math.random() * 100 - 50,
|
||||||
|
y: parentNode.position('y') + Math.random() * 100 - 50
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Zufällige Position im sichtbaren Bereich
|
||||||
|
const extent = cy.extent();
|
||||||
|
position = {
|
||||||
|
x: extent.x1 + Math.random() * (extent.x2 - extent.x1),
|
||||||
|
y: extent.y1 + Math.random() * (extent.y2 - extent.y1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten zur Mindmap hinzufügen
|
||||||
|
fetch(`/api/mindmap/${mindmapId}/add_node`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
node_id: nodeId,
|
||||||
|
parent_id: parentNodeId || null,
|
||||||
|
x: position.x,
|
||||||
|
y: position.y
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showUINotification('Knoten erfolgreich hinzugefügt', 'success');
|
||||||
|
document.getElementById('add-node-dialog-overlay').remove();
|
||||||
|
|
||||||
|
// Aktualisiere die Mindmap (neu laden oder Knoten hinzufügen)
|
||||||
|
// Option 1: Seite neu laden
|
||||||
|
// window.location.reload();
|
||||||
|
|
||||||
|
// Option 2: Knoten dynamisch hinzufügen (eleganter)
|
||||||
|
fetch(`/api/mindmap/node/${nodeId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(nodeData => {
|
||||||
|
// Füge den neuen Knoten zur Anzeige hinzu
|
||||||
|
cy.add({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: nodeId,
|
||||||
|
label: nodeData.name,
|
||||||
|
description: nodeData.description,
|
||||||
|
color: nodeData.color_code,
|
||||||
|
fontColor: '#FFFFFF'
|
||||||
|
},
|
||||||
|
position: position
|
||||||
|
});
|
||||||
|
|
||||||
|
// Füge auch die Kante hinzu, falls ein Elternknoten ausgewählt wurde
|
||||||
|
if (parentNodeId) {
|
||||||
|
cy.add({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
source: parentNodeId,
|
||||||
|
target: nodeId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wähle den neuen Knoten aus
|
||||||
|
cy.$id(nodeId).select();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Knotendaten:', error);
|
||||||
|
// Fallback: Seite neu laden
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
showUINotification('Fehler beim Hinzufügen des Knotens: ' + (data.message || data.error), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Hinzufügen des Knotens:', error);
|
||||||
|
showUINotification('Fehler beim Hinzufügen des Knotens', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Fallback, falls mindmapData null ist
|
||||||
|
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
|
||||||
|
cyContainer.innerHTML = '<p class="text-center text-red-500 p-10">Die angeforderte Mindmap konnte nicht geladen werden.</p>';
|
||||||
|
}
|
||||||
|
}); // End of DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,19 +11,33 @@ import importlib.util
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app, db_path
|
# Direkt den Datenbankpfad berechnen, statt ihn aus app.py zu importieren
|
||||||
|
def get_db_path():
|
||||||
|
"""Berechnet den absoluten Pfad zur Datenbank"""
|
||||||
|
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
return os.path.join(basedir, 'database', 'systades.db')
|
||||||
|
|
||||||
|
# Import models direkt
|
||||||
from models import db
|
from models import db
|
||||||
|
|
||||||
def ensure_db_dir():
|
def ensure_db_dir():
|
||||||
"""Make sure the database directory exists."""
|
"""Make sure the database directory exists."""
|
||||||
|
db_path = get_db_path()
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
def fix_database_schema():
|
def fix_database_schema():
|
||||||
"""Fix the database schema by adding missing columns."""
|
"""Fix the database schema by adding missing columns."""
|
||||||
|
# Import Flask-App erst innerhalb der Funktion
|
||||||
|
from flask import Flask
|
||||||
|
from app import app
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Ensure directory exists
|
# Ensure directory exists
|
||||||
ensure_db_dir()
|
ensure_db_dir()
|
||||||
|
|
||||||
|
# Get database path
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
# Check if database exists, create tables if needed
|
# Check if database exists, create tables if needed
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
print("Database doesn't exist. Creating all tables from scratch...")
|
print("Database doesn't exist. Creating all tables from scratch...")
|
||||||
|
|||||||
Reference in New Issue
Block a user