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 '
'; - print ' '.$toggleLabel.''; + if ($allowedViews === 'both') { + print ' '.$toggleLabel.''; + } if ($permissiontoadd) { print ' '.$langs->trans('AddElement').''; print ' '.$langs->trans('AddConnection').''; diff --git a/sql/llx_c_kundenkarte_anlage_system.sql b/sql/llx_c_kundenkarte_anlage_system.sql index 3889add..32db9e7 100755 --- a/sql/llx_c_kundenkarte_anlage_system.sql +++ b/sql/llx_c_kundenkarte_anlage_system.sql @@ -19,6 +19,9 @@ CREATE TABLE llx_c_kundenkarte_anlage_system -- Tree display configuration (JSON) tree_display_config text COMMENT 'JSON config for tree display options', + -- Verfügbare Ansichten: tree, graph, both + view_modes varchar(20) NOT NULL DEFAULT 'both', + position integer DEFAULT 0, active tinyint DEFAULT 1 NOT NULL ) ENGINE=innodb; diff --git a/tabs/anlagen.php b/tabs/anlagen.php index d463f22..dbf18ae 100755 --- a/tabs/anlagen.php +++ b/tabs/anlagen.php @@ -366,12 +366,23 @@ if ($action == 'togglepin' && $permissiontoadd) { $title = $langs->trans('TechnicalInstallations').' - '.$object->name; -// Ansichtsmodus: URL-Parameter hat Vorrang, sonst Admin-Setting +// Ansichtsmodus: pro System konfigurierbar (tree/graph/both) +$allowedViews = 'both'; +if ($systemId > 0 && isset($customerSystems[$systemId]) && !empty($customerSystems[$systemId]->view_modes)) { + $allowedViews = $customerSystems[$systemId]->view_modes; +} + $defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'); $viewMode = GETPOST('view', 'aZ09'); if (!in_array($viewMode, array('tree', 'graph'))) { $viewMode = $defaultView; } +// Erzwinge erlaubten Modus wenn nur einer verfügbar +if ($allowedViews === 'tree') { + $viewMode = 'tree'; +} elseif ($allowedViews === 'graph') { + $viewMode = 'graph'; +} dol_include_once('/kundenkarte/lib/graph_view.lib.php'); @@ -390,6 +401,14 @@ print dol_get_fiche_head($head, 'anlagen', $langs->trans("ThirdParty"), -1, 'com $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom'); +// Admin-Schnellzugriff (nur für Modul-Admins) +if ($user->admin || $user->hasRight('kundenkarte', 'admin')) { + $adminUrl = dol_buildpath('/kundenkarte/admin/setup.php', 1); + print '
'; + print ''; + print '
'; +} + print '
'; // Confirmation dialogs @@ -474,9 +493,11 @@ if ($isTreeView) { if ($viewMode !== 'graph') { // Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper) print '
'; - print ''; - print ' '.$toggleLabel; - print ''; + if ($allowedViews === 'both') { + print ''; + print ' '.$toggleLabel; + print ''; + } print ''; @@ -505,6 +526,7 @@ if ($isTreeView && $viewMode === 'graph') { 'contactid' => 0, 'systemid' => $systemId, 'viewMode' => $viewMode, + 'allowedViews' => $allowedViews, 'permissiontoadd' => $permissiontoadd, 'pageUrl' => $_SERVER['PHP_SELF'], )); @@ -1066,18 +1088,8 @@ if (empty($customerSystems)) { $db->free($resql); } - // Pre-load all connections for this customer/system - dol_include_once('/kundenkarte/class/anlageconnection.class.php'); - $connObj = new AnlageConnection($db); - $allConnections = $connObj->fetchBySociete($id, $systemId); - // Index by target_id for quick lookup (connection shows ABOVE the target element) + // Verbindungen werden nur im Graph angezeigt, nicht im Baum $connectionsByTarget = array(); - foreach ($allConnections as $conn) { - if (!isset($connectionsByTarget[$conn->fk_target])) { - $connectionsByTarget[$conn->fk_target] = array(); - } - $connectionsByTarget[$conn->fk_target][] = $conn; - } if (!empty($tree)) { print '
'; @@ -1289,7 +1301,7 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev print ''; if ($canEdit) { print ''; - print ''; + print ''; print ''; } @@ -1553,7 +1565,7 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete print ''; if ($canEdit) { print ''; - print ''; + print ''; print ''; } diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php index 7ef06c6..e727144 100755 --- a/tabs/contact_anlagen.php +++ b/tabs/contact_anlagen.php @@ -365,13 +365,24 @@ if ($action == 'togglepin' && $permissiontoadd) { $title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs); -// Ansichtsmodus: URL-Parameter hat Vorrang, sonst Admin-Setting +// Ansichtsmodus: pro System konfigurierbar (tree/graph/both) dol_include_once('/kundenkarte/lib/graph_view.lib.php'); +$allowedViews = 'both'; +if ($systemId > 0 && isset($customerSystems[$systemId]) && !empty($customerSystems[$systemId]->view_modes)) { + $allowedViews = $customerSystems[$systemId]->view_modes; +} + $defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'); $viewMode = GETPOST('view', 'aZ09'); if (!in_array($viewMode, array('tree', 'graph'))) { $viewMode = $defaultView; } +// Erzwinge erlaubten Modus wenn nur einer verfügbar +if ($allowedViews === 'tree') { + $viewMode = 'tree'; +} elseif ($allowedViews === 'graph') { + $viewMode = 'graph'; +} $jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time()); $cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time()); @@ -388,6 +399,14 @@ print dol_get_fiche_head($head, 'anlagen', $langs->trans("ContactAddress"), -1, $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'nom'); +// Admin-Schnellzugriff (nur für Modul-Admins) +if ($user->admin || $user->hasRight('kundenkarte', 'admin')) { + $adminUrl = dol_buildpath('/kundenkarte/admin/setup.php', 1); + print '
'; + print ''; + print '
'; +} + print '
'; // Confirmation dialogs @@ -472,9 +491,11 @@ if ($isTreeView) { if ($viewMode !== 'graph') { // Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper) print '
'; - print ''; - print ' '.$toggleLabel; - print ''; + if ($allowedViews === 'both') { + print ''; + print ' '.$toggleLabel; + print ''; + } print ''; @@ -503,6 +524,7 @@ if ($isTreeView && $viewMode === 'graph') { 'contactid' => $id, 'systemid' => $systemId, 'viewMode' => $viewMode, + 'allowedViews' => $allowedViews, 'permissiontoadd' => $permissiontoadd, 'pageUrl' => $_SERVER['PHP_SELF'], )); @@ -1064,18 +1086,8 @@ if (empty($customerSystems)) { $db->free($resql); } - // Pre-load all connections for this contact/system - dol_include_once('/kundenkarte/class/anlageconnection.class.php'); - $connObj = new AnlageConnection($db); - $allConnections = $connObj->fetchBySociete($object->socid, $systemId); - // Index by target_id for quick lookup (connection shows ABOVE the target element) + // Verbindungen werden nur im Graph angezeigt, nicht im Baum $connectionsByTarget = array(); - foreach ($allConnections as $conn) { - if (!isset($connectionsByTarget[$conn->fk_target])) { - $connectionsByTarget[$conn->fk_target] = array(); - } - $connectionsByTarget[$conn->fk_target][] = $conn; - } if (!empty($tree)) { print '
'; @@ -1287,7 +1299,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, print ''; if ($canEdit) { print ''; - print ''; + print ''; print ''; } @@ -1583,7 +1595,7 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe print ''; if ($canEdit) { print ''; - print ''; + print ''; print ''; }