diff --git a/admin/anlage_systems.php b/admin/anlage_systems.php
index b4054a1..f614c88 100755
--- a/admin/anlage_systems.php
+++ b/admin/anlage_systems.php
@@ -36,6 +36,8 @@ if ($action == 'add') {
$picto = GETPOST('picto', 'alphanohtml');
$color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position');
+ $viewModes = GETPOST('view_modes', 'aZ09');
+ if (!in_array($viewModes, array('tree', 'graph', 'both'))) $viewModes = 'both';
// Tree display config
$treeConfig = array(
@@ -54,12 +56,13 @@ if ($action == 'add') {
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
} else {
$sql = "INSERT INTO ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
- $sql .= " (code, label, picto, color, position, active, entity, tree_display_config)";
+ $sql .= " (code, label, picto, color, position, active, entity, tree_display_config, view_modes)";
$sql .= " VALUES ('".$db->escape(strtoupper($code))."', '".$db->escape($label)."',";
$sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").",";
$sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").",";
$sql .= " ".((int) $position).", 1, 0,";
- $sql .= " '".$db->escape($treeConfigJson)."')";
+ $sql .= " '".$db->escape($treeConfigJson)."',";
+ $sql .= " '".$db->escape($viewModes)."')";
$result = $db->query($sql);
if ($result) {
@@ -77,6 +80,8 @@ if ($action == 'update') {
$picto = GETPOST('picto', 'alphanohtml');
$color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position');
+ $viewModes = GETPOST('view_modes', 'aZ09');
+ if (!in_array($viewModes, array('tree', 'graph', 'both'))) $viewModes = 'both';
// Tree display config
$treeConfig = array(
@@ -98,6 +103,7 @@ if ($action == 'update') {
$sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL");
$sql .= ", position = ".((int) $position);
$sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'";
+ $sql .= ", view_modes = '".$db->escape($viewModes)."'";
$sql .= " WHERE rowid = ".((int) $systemId);
$result = $db->query($sql);
@@ -212,6 +218,21 @@ if ($action == 'create' || $action == 'edit') {
print '
| '.$langs->trans('Position').' | ';
print ' |
';
+ // Verfügbare Ansichten
+ $currentViewModes = ($system && !empty($system->view_modes)) ? $system->view_modes : 'both';
+ print '| '.$langs->trans('ViewModes').' | ';
+ print ' |
';
+
print '';
// Tree display configuration
@@ -304,6 +325,7 @@ if ($action == 'create' || $action == 'edit') {
print ''.$langs->trans('SystemCode').' | ';
print ''.$langs->trans('SystemLabel').' | ';
print ''.$langs->trans('SystemPicto').' | ';
+ print ''.$langs->trans('ViewModes').' | ';
print ''.$langs->trans('Position').' | ';
print ''.$langs->trans('Status').' | ';
print ''.$langs->trans('Actions').' | ';
@@ -321,6 +343,10 @@ if ($action == 'create' || $action == 'edit') {
print dol_escape_htmltag($obj->picto);
}
print '';
+ // Ansichtsmodus
+ $vmLabel = array('both' => 'Baum & Graph', 'tree' => 'Nur Baum', 'graph' => 'Nur Graph');
+ $vm = !empty($obj->view_modes) ? $obj->view_modes : 'both';
+ print ''.($vmLabel[$vm] ?? $vm).' | ';
print ''.$obj->position.' | ';
print '';
diff --git a/ajax/graph_data.php b/ajax/graph_data.php
index 0646738..c3758d2 100755
--- a/ajax/graph_data.php
+++ b/ajax/graph_data.php
@@ -67,8 +67,9 @@ if ($resFields) {
// Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL)
// Hierarchie kommt aus fk_parent (wie im Baum)
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type,";
-$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y,";
+$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y, a.graph_width, a.graph_height,";
$sql .= " t.label as type_label, t.picto as type_picto, t.color as type_color,";
+$sql .= " t.can_have_children as type_can_have_children,";
$sql .= " s.code as system_code, s.label as system_label,";
$sql .= " ts.code as type_system_code,";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('image/jpeg','image/png','image/gif','image/webp')) as image_count,";
@@ -90,8 +91,9 @@ $sql .= " ORDER BY a.fk_parent, a.rang, a.rowid";
$elements = array('nodes' => array(), 'edges' => array());
$nodeIds = array();
-// Zwischenspeicher: rowid → isBuilding (für Compound-Entscheidung)
$nodeIsBuilding = array();
+// Hierarchie: rowid → fk_parent (zum Traversieren nach oben)
+$nodeParentMap = array();
// Hierarchie-Kanten (Gerät→Gerät), werden durch echte Kabel ersetzt falls vorhanden
$hierarchyEdges = array();
// Zwischenspeicher: alle DB-Zeilen für Zwei-Pass-Verarbeitung
@@ -99,10 +101,11 @@ $rows = array();
$resql = $db->query($sql);
if ($resql) {
- // 1. Pass: Alle Zeilen laden und Gebäude-Typen merken
+ // 1. Pass: Alle Zeilen laden, Gebäude-Typen und Hierarchie merken
while ($obj = $db->fetch_object($resql)) {
$isBuilding = (!empty($obj->type_system_code) && $obj->type_system_code === 'GLOBAL');
$nodeIsBuilding[(int)$obj->rowid] = $isBuilding;
+ $nodeParentMap[(int)$obj->rowid] = (int)$obj->fk_parent;
$nodeIds[$obj->rowid] = true;
$rows[] = $obj;
}
@@ -129,6 +132,8 @@ if ($resql) {
'doc_count' => (int) $obj->doc_count,
'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null,
'graph_y' => $obj->graph_y !== null ? (float) $obj->graph_y : null,
+ 'graph_width' => $obj->graph_width !== null ? (float) $obj->graph_width : null,
+ 'graph_height' => $obj->graph_height !== null ? (float) $obj->graph_height : null,
);
// Feldwerte mit Metadaten (Label, Display-Modus, Badge-Farbe)
@@ -169,16 +174,13 @@ if ($resql) {
}
}
- // Compound-Parent: NUR wenn Eltern-Node ein Gebäude/Raum ist
+ // Compound-Parent: Nächstes Gebäude in der Hierarchie nach oben finden
+ // Alle Nodes innerhalb eines Gebäudes werden von diesem umschlossen
// Gerät→Gerät Hierarchie wird als Kante dargestellt (nicht verschachtelt)
$parentId = (int) $obj->fk_parent;
if ($parentId > 0 && isset($nodeIds[$parentId])) {
- $parentIsBuilding = !empty($nodeIsBuilding[$parentId]);
- if ($parentIsBuilding) {
- // Gebäude/Raum als Container → Compound-Parent
- $nodeData['parent'] = 'n_'.$parentId;
- } else {
- // Gerät→Gerät → Hierarchie-Kante vormerken (wird ggf. durch Kabel ersetzt)
+ // Hierarchie-Kante zum direkten Parent (wird ggf. durch Kabel ersetzt)
+ if (!$isBuilding) {
$hierKey = min($parentId, (int)$obj->rowid).'_'.max($parentId, (int)$obj->rowid);
$hierarchyEdges[$hierKey] = array(
'data' => array(
@@ -190,6 +192,17 @@ if ($resql) {
'classes' => 'hierarchy-edge'
);
}
+
+ // Nach oben traversieren bis ein Gebäude gefunden wird → Compound-Parent
+ $ancestorId = $parentId;
+ $maxDepth = 20; // Endlosschleifen vermeiden
+ while ($ancestorId > 0 && $maxDepth-- > 0) {
+ if (!empty($nodeIsBuilding[$ancestorId])) {
+ $nodeData['parent'] = 'n_'.$ancestorId;
+ break;
+ }
+ $ancestorId = isset($nodeParentMap[$ancestorId]) ? $nodeParentMap[$ancestorId] : 0;
+ }
}
$elements['nodes'][] = array(
diff --git a/ajax/graph_save_positions.php b/ajax/graph_save_positions.php
index 9142cef..eab5d83 100755
--- a/ajax/graph_save_positions.php
+++ b/ajax/graph_save_positions.php
@@ -51,6 +51,12 @@ if ($action === 'save') {
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
$sql .= " SET graph_x = ".$x.", graph_y = ".$y;
+ // Gebäude-Größe optional mitspeichern
+ if (isset($pos['w']) && isset($pos['h'])) {
+ $w = (float) $pos['w'];
+ $h = (float) $pos['h'];
+ $sql .= ", graph_width = ".($w > 0 ? $w : "NULL").", graph_height = ".($h > 0 ? $h : "NULL");
+ }
$sql .= " WHERE rowid = ".$anlageId;
if ($db->query($sql)) {
$saved++;
@@ -73,7 +79,7 @@ if ($action === 'save') {
}
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
- $sql .= " SET graph_x = NULL, graph_y = NULL";
+ $sql .= " SET graph_x = NULL, graph_y = NULL, graph_width = NULL, graph_height = NULL";
$sql .= " WHERE fk_soc = ".(int)$socId;
if ($contactId > 0) {
$sql .= " AND fk_contact = ".(int)$contactId;
diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php
index 0281472..73ea888 100755
--- a/core/modules/modKundenKarte.class.php
+++ b/core/modules/modKundenKarte.class.php
@@ -622,6 +622,12 @@ class modKundenKarte extends DolibarrModules
// v5.2.0: Halbe TE-Breiten (4.5 TE für Neozed etc.)
$this->migrate_v520_decimal_te();
+ // v5.3.0: Ansichtsmodus pro System (tree/graph/both)
+ $this->migrate_v530_system_view_modes();
+
+ // v5.3.0: Gebäude-Größe im Graph speichern
+ $this->migrate_v530_graph_dimensions();
+
// v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge
$this->migrate_v680_bundled_terminals();
@@ -835,6 +841,37 @@ class modKundenKarte extends DolibarrModules
}
}
+ /**
+ * Migration v5.3.0: Spalte view_modes für Ansichtsmodus pro System
+ */
+ private function migrate_v530_system_view_modes()
+ {
+ $table = MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
+ $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
+ if ($resql && $this->db->num_rows($resql) > 0) {
+ $resql = $this->db->query("SHOW COLUMNS FROM ".$table." WHERE Field = 'view_modes'");
+ if ($resql && $this->db->num_rows($resql) == 0) {
+ $this->db->query("ALTER TABLE ".$table." ADD COLUMN view_modes VARCHAR(20) NOT NULL DEFAULT 'both' AFTER tree_display_config");
+ }
+ }
+ }
+
+ /**
+ * Migration v5.3.0: Gebäude-Größe im Graph speichern (graph_width, graph_height)
+ */
+ private function migrate_v530_graph_dimensions()
+ {
+ $table = MAIN_DB_PREFIX."kundenkarte_anlage";
+ $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
+ if ($resql && $this->db->num_rows($resql) > 0) {
+ $resql = $this->db->query("SHOW COLUMNS FROM ".$table." WHERE Field = 'graph_width'");
+ if ($resql && $this->db->num_rows($resql) == 0) {
+ $this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_width DECIMAL(10,2) DEFAULT NULL AFTER graph_y");
+ $this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_height DECIMAL(10,2) DEFAULT NULL AFTER graph_width");
+ }
+ }
+ }
+
/**
* Migration v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge
* Ermöglicht einen Abgang der alle Terminals eines breiten Equipment belegt
diff --git a/css/kundenkarte_cytoscape.css b/css/kundenkarte_cytoscape.css
index cfe401f..0528c99 100755
--- a/css/kundenkarte_cytoscape.css
+++ b/css/kundenkarte_cytoscape.css
@@ -400,3 +400,30 @@
width: 160px;
}
}
+
+/* ==========================================
+ * Resize-Handles für Gebäude-Compound-Nodes
+ * ========================================== */
+.kundenkarte-resize-handle {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ background: rgba(67, 144, 220, 0.6);
+ border: 1px solid #4390dc;
+ border-radius: 2px;
+ z-index: 100;
+ box-sizing: border-box;
+}
+.kundenkarte-resize-handle:hover {
+ background: rgba(67, 144, 220, 0.9);
+}
+
+/* Cursor je nach Handle-Position */
+.kundenkarte-resize-handle.resize-nw { cursor: nw-resize; }
+.kundenkarte-resize-handle.resize-n { cursor: n-resize; }
+.kundenkarte-resize-handle.resize-ne { cursor: ne-resize; }
+.kundenkarte-resize-handle.resize-e { cursor: e-resize; }
+.kundenkarte-resize-handle.resize-se { cursor: se-resize; }
+.kundenkarte-resize-handle.resize-s { cursor: s-resize; }
+.kundenkarte-resize-handle.resize-sw { cursor: sw-resize; }
+.kundenkarte-resize-handle.resize-w { cursor: w-resize; }
diff --git a/js/kundenkarte_cytoscape.js b/js/kundenkarte_cytoscape.js
index a600ddd..bb47565 100755
--- a/js/kundenkarte_cytoscape.js
+++ b/js/kundenkarte_cytoscape.js
@@ -460,8 +460,8 @@
'text-background-opacity': 0.95,
'text-background-padding': '4px',
'text-background-shape': 'roundrectangle',
- 'min-width': '120px',
- 'min-height': '60px'
+ 'min-width': function(node) { return node.data('graph_width') || 120; },
+ 'min-height': function(node) { return node.data('graph_height') || 60; }
}
},
// Geräte - Karten-Design mit Feldwerten (kompakt)
@@ -754,9 +754,14 @@
var snappedY = Math.round(pos.y / grid) * grid;
node.position({ x: snappedX, y: snappedY });
- // Position zum Speichern vormerken
+ // Position zum Speichern vormerken (bei Buildings auch Größe)
var anlageId = node.data('id').replace('n_', '');
- self._dirtyNodes[anlageId] = { id: parseInt(anlageId), x: snappedX, y: snappedY };
+ var dirtyEntry = { id: parseInt(anlageId), x: snappedX, y: snappedY };
+ if (node.hasClass('building-node') && node.data('graph_width')) {
+ dirtyEntry.w = node.data('graph_width');
+ dirtyEntry.h = node.data('graph_height');
+ }
+ self._dirtyNodes[anlageId] = dirtyEntry;
// Bei Compound-Nodes auch alle Kinder speichern
var children = node.children();
@@ -886,13 +891,20 @@
this.editMode = true;
this._dirtyNodes = {};
- // Aktuelle Positionen sichern (für Abbrechen)
+ // Aktuelle Positionen und Größen sichern (für Abbrechen)
this._savedPositions = {};
+ this._savedSizes = {};
var self = this;
this.cy.nodes().forEach(function(node) {
var pos = node.position();
self._savedPositions[node.data('id')] = { x: pos.x, y: pos.y };
});
+ this.cy.nodes('.building-node').forEach(function(node) {
+ self._savedSizes[node.data('id')] = {
+ w: node.data('graph_width'),
+ h: node.data('graph_height')
+ };
+ });
// Nodes verschiebbar machen
this.cy.autoungrabify(false);
@@ -903,6 +915,9 @@
// Visuelles Feedback: Container-Rahmen
$('#' + this.containerId).addClass('graph-edit-mode');
+
+ // Resize-Handles für Gebäude
+ this.showResizeHandles();
},
/**
@@ -912,6 +927,7 @@
if (!this.cy) return;
this.editMode = false;
this._savedPositions = {};
+ this._savedSizes = {};
// Nodes wieder sperren
this.cy.autoungrabify(true);
@@ -922,10 +938,13 @@
// Visuelles Feedback entfernen
$('#' + this.containerId).removeClass('graph-edit-mode');
+
+ // Resize-Handles entfernen
+ this.hideResizeHandles();
},
/**
- * Bearbeitungsmodus abbrechen - Positionen zurücksetzen
+ * Bearbeitungsmodus abbrechen - Positionen und Größen zurücksetzen
*/
cancelEditMode: function() {
if (!this.cy) return;
@@ -939,10 +958,228 @@
}
});
+ // Gebäude-Größen zurücksetzen
+ this.cy.nodes('.building-node').forEach(function(node) {
+ var saved = self._savedSizes[node.data('id')];
+ if (saved) {
+ node.data('graph_width', saved.w);
+ node.data('graph_height', saved.h);
+ node.style({
+ 'min-width': saved.w || 120,
+ 'min-height': saved.h || 60
+ });
+ }
+ });
+
this._dirtyNodes = {};
this.exitEditMode();
},
+ // ==========================================
+ // Resize-Handles für Gebäude-Compound-Nodes
+ // ==========================================
+
+ _resizeHandles: [],
+ _resizeListeners: null,
+
+ /**
+ * Resize-Handles für alle Building-Nodes anzeigen
+ */
+ showResizeHandles: function() {
+ var self = this;
+ var $container = $('#' + this.containerId);
+
+ // Container relativ positionieren falls nötig
+ if ($container.css('position') === 'static') {
+ $container.css('position', 'relative');
+ }
+
+ // Handles für jedes Gebäude erstellen
+ this.cy.nodes('.building-node').forEach(function(node) {
+ self._createHandlesForNode(node, $container);
+ });
+
+ // Viewport-Änderungen: Handles repositionieren
+ this._resizeListeners = {
+ viewport: function() { self.updateResizeHandles(); },
+ position: function() { self.updateResizeHandles(); },
+ // Nach Drag repositionieren
+ dragfree: function() { self.updateResizeHandles(); }
+ };
+ this.cy.on('viewport', this._resizeListeners.viewport);
+ this.cy.on('position', 'node', this._resizeListeners.position);
+ this.cy.on('dragfree', 'node', this._resizeListeners.dragfree);
+ },
+
+ /**
+ * Handles für einen einzelnen Building-Node erstellen
+ */
+ _createHandlesForNode: function(node, $container) {
+ var self = this;
+ var nodeId = node.data('id');
+ // 4 Ecken + 4 Kanten
+ var positions = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
+
+ positions.forEach(function(pos) {
+ var $handle = $('');
+ $container.append($handle);
+ self._resizeHandles.push($handle[0]);
+
+ $handle.on('mousedown', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ self._startResize(node, pos, e);
+ });
+ });
+
+ this._positionHandles(node);
+ },
+
+ /**
+ * Handles eines Nodes an die aktuelle Bounding-Box positionieren
+ */
+ _positionHandles: function(node) {
+ var nodeId = node.data('id');
+ var bb = node.renderedBoundingBox({ includeLabels: false });
+ var $container = $('#' + this.containerId);
+ var containerOffset = $container.offset();
+ var canvasOffset = $container.find('canvas').first().offset() || containerOffset;
+ // Offset relativ zum Container
+ var ox = canvasOffset.left - containerOffset.left;
+ var oy = canvasOffset.top - containerOffset.top;
+
+ var handleSize = 10;
+ var half = handleSize / 2;
+
+ var coords = {
+ 'nw': { left: bb.x1 + ox - half, top: bb.y1 + oy - half },
+ 'n': { left: bb.x1 + ox + (bb.w / 2) - half, top: bb.y1 + oy - half },
+ 'ne': { left: bb.x2 + ox - half, top: bb.y1 + oy - half },
+ 'e': { left: bb.x2 + ox - half, top: bb.y1 + oy + (bb.h / 2) - half },
+ 'se': { left: bb.x2 + ox - half, top: bb.y2 + oy - half },
+ 's': { left: bb.x1 + ox + (bb.w / 2) - half, top: bb.y2 + oy - half },
+ 'sw': { left: bb.x1 + ox - half, top: bb.y2 + oy - half },
+ 'w': { left: bb.x1 + ox - half, top: bb.y1 + oy + (bb.h / 2) - half }
+ };
+
+ $('.kundenkarte-resize-handle[data-node="' + nodeId + '"]').each(function() {
+ var pos = $(this).data('pos');
+ if (coords[pos]) {
+ $(this).css({ left: coords[pos].left + 'px', top: coords[pos].top + 'px' });
+ }
+ });
+ },
+
+ /**
+ * Alle Handles repositionieren (nach Zoom/Pan/Drag)
+ */
+ updateResizeHandles: function() {
+ var self = this;
+ if (this._updateHandleTimer) clearTimeout(this._updateHandleTimer);
+ this._updateHandleTimer = setTimeout(function() {
+ self.cy.nodes('.building-node').forEach(function(node) {
+ self._positionHandles(node);
+ });
+ }, 16); // ~60fps
+ },
+
+ /**
+ * Resize starten bei mousedown auf Handle
+ */
+ _startResize: function(node, handlePos, startEvent) {
+ var self = this;
+ var startX = startEvent.clientX;
+ var startY = startEvent.clientY;
+ var zoom = this.cy.zoom();
+
+ // Aktuelle Größe (model-Koordinaten, nicht rendered)
+ var bb = node.boundingBox({ includeLabels: false });
+ var startW = bb.w;
+ var startH = bb.h;
+
+ // Minimale Größe
+ var minW = 80;
+ var minH = 40;
+
+ // Node während Resize nicht verschiebbar
+ node.ungrabify();
+
+ function onMouseMove(e) {
+ var dx = (e.clientX - startX) / zoom;
+ var dy = (e.clientY - startY) / zoom;
+
+ var newW = startW;
+ var newH = startH;
+
+ // Breite anpassen je nach Handle-Position
+ if (handlePos.indexOf('e') >= 0) newW = startW + dx;
+ if (handlePos.indexOf('w') >= 0) newW = startW - dx;
+ // Höhe anpassen
+ if (handlePos.indexOf('s') >= 0) newH = startH + dy;
+ if (handlePos.indexOf('n') >= 0) newH = startH - dy;
+
+ // Minimum
+ newW = Math.max(minW, newW);
+ newH = Math.max(minH, newH);
+
+ // Auf Grid rasten
+ newW = Math.round(newW / self.gridSize) * self.gridSize;
+ newH = Math.round(newH / self.gridSize) * self.gridSize;
+
+ // Cytoscape Style aktualisieren
+ node.style({ 'min-width': newW, 'min-height': newH });
+ node.data('graph_width', newW);
+ node.data('graph_height', newH);
+
+ // Handles repositionieren
+ self._positionHandles(node);
+ }
+
+ function onMouseUp() {
+ $(document).off('mousemove', onMouseMove);
+ $(document).off('mouseup', onMouseUp);
+
+ // Node wieder verschiebbar
+ node.grabify();
+
+ // Endgültige Größe merken
+ var anlageId = node.data('id').replace('n_', '');
+ var pos = node.position();
+ self._dirtyNodes[anlageId] = {
+ id: parseInt(anlageId),
+ x: pos.x,
+ y: pos.y,
+ w: node.data('graph_width') || 0,
+ h: node.data('graph_height') || 0
+ };
+
+ self.updateResizeHandles();
+ }
+
+ $(document).on('mousemove', onMouseMove);
+ $(document).on('mouseup', onMouseUp);
+ },
+
+ /**
+ * Alle Resize-Handles entfernen
+ */
+ hideResizeHandles: function() {
+ $('.kundenkarte-resize-handle').remove();
+ this._resizeHandles = [];
+ if (this._updateHandleTimer) {
+ clearTimeout(this._updateHandleTimer);
+ this._updateHandleTimer = null;
+ }
+
+ // Event-Listener entfernen
+ if (this._resizeListeners && this.cy) {
+ this.cy.off('viewport', this._resizeListeners.viewport);
+ this.cy.off('position', 'node', this._resizeListeners.position);
+ this.cy.off('dragfree', 'node', this._resizeListeners.dragfree);
+ this._resizeListeners = null;
+ }
+ },
+
/**
* Geänderte Positionen an Server senden
*/
diff --git a/langs/de_DE/kundenkarte.lang b/langs/de_DE/kundenkarte.lang
index 30dde97..6621ecd 100755
--- a/langs/de_DE/kundenkarte.lang
+++ b/langs/de_DE/kundenkarte.lang
@@ -549,3 +549,9 @@ TreeView = Baumansicht
GraphView = Graph-Ansicht
GraphLoading = Graph wird geladen...
SearchPlaceholder = Suchen...
+
+# Ansichtsmodus pro System
+ViewModes = Verfuegbare Ansichten
+ViewModesBoth = Baum & Graph
+ViewModesTreeOnly = Nur Baum
+ViewModesGraphOnly = Nur Graph
diff --git a/langs/en_US/kundenkarte.lang b/langs/en_US/kundenkarte.lang
index 3501d3c..7d5aef8 100755
--- a/langs/en_US/kundenkarte.lang
+++ b/langs/en_US/kundenkarte.lang
@@ -297,3 +297,9 @@ TreeView = Tree View
GraphView = Graph View
GraphLoading = Loading graph...
SearchPlaceholder = Search...
+
+# View modes per system
+ViewModes = Available Views
+ViewModesBoth = Tree & Graph
+ViewModesTreeOnly = Tree Only
+ViewModesGraphOnly = Graph Only
diff --git a/lib/graph_view.lib.php b/lib/graph_view.lib.php
index 89713fa..b4642f8 100755
--- a/lib/graph_view.lib.php
+++ b/lib/graph_view.lib.php
@@ -47,6 +47,7 @@ function kundenkarte_graph_print_toolbar($params)
$viewMode = $params['viewMode'] ?? 'tree';
$permissiontoadd = !empty($params['permissiontoadd']);
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
+ $allowedViews = $params['allowedViews'] ?? 'both';
// View-Toggle URL: ID-Parameter je nach Kontext
$idParam = ($contactId > 0) ? $contactId : $socId;
@@ -65,7 +66,9 @@ function kundenkarte_graph_print_toolbar($params)
// Zeile 1: Ansicht-Wechsel + Aktionen
print ' |