diff --git a/ChangeLog.md b/ChangeLog.md index 1afd3d0..87153e2 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,22 @@ # CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 5.1.0 (2026-02) + +### Verbesserungen +- **Graph-Ansicht: Intelligente Feldanzeige** + - Felder nach `position` sortiert (nicht mehr nach JSON-Reihenfolge) + - Nur Felder mit `show_in_tree=1` werden auf den Graph-Nodes angezeigt + - Nur Felder mit `show_in_hover=1` erscheinen im Tooltip + - Badge-Werte im Graph mit Feldbezeichnung (z.B. "Hersteller: ABB") + - Tooltip: Typ/System entfernt (redundant mit Graph-Node) + - Tooltip: Farbige Badge-Kaesten wie in der Baumansicht + - Shared Library `lib/graph_view.lib.php` fuer Toolbar/Container/Legende + +### Neue Dateien +- `lib/graph_view.lib.php` - Gemeinsame Graph-Funktionen (Toolbar, Container, Legende) + +--- + ## 5.0.0 (2026-02) ### Neue Features diff --git a/ajax/graph_data.php b/ajax/graph_data.php old mode 100644 new mode 100755 index 6e83b7b..0646738 --- a/ajax/graph_data.php +++ b/ajax/graph_data.php @@ -43,8 +43,9 @@ if ($socId <= 0) { } // Feld-Metadaten laden (field_code → Label, Display-Modus, Badge-Farbe pro Typ) -$fieldMeta = array(); // [fk_anlage_type][field_code] = {label, display_mode, badge_color} -$sqlFields = "SELECT fk_anlage_type, field_code, field_label, field_type, tree_display_mode, badge_color"; +// Sortiert nach position → Reihenfolge wird in der Graph-Ansicht beibehalten +$fieldMeta = array(); // [fk_anlage_type][field_code] = {label, display_mode, badge_color, show_in_tree, show_in_hover} +$sqlFields = "SELECT fk_anlage_type, field_code, field_label, field_type, tree_display_mode, badge_color, show_in_tree, show_in_hover"; $sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field WHERE active = 1 ORDER BY position"; $resFields = $db->query($sqlFields); if ($resFields) { @@ -55,6 +56,8 @@ if ($resFields) { 'display' => $fObj->tree_display_mode ?: 'badge', 'color' => $fObj->badge_color ?: '', 'type' => $fObj->field_type, + 'show_in_tree' => (int) $fObj->show_in_tree, + 'show_in_hover' => (int) $fObj->show_in_hover, ); } $db->free($resFields); @@ -87,15 +90,29 @@ $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-Kanten (Gerät→Gerät), werden durch echte Kabel ersetzt falls vorhanden +$hierarchyEdges = array(); +// Zwischenspeicher: alle DB-Zeilen für Zwei-Pass-Verarbeitung +$rows = array(); $resql = $db->query($sql); if ($resql) { + // 1. Pass: Alle Zeilen laden und Gebäude-Typen merken while ($obj = $db->fetch_object($resql)) { - // Typ bestimmt ob Raum oder Gerät (GLOBAL-Typ = Gebäude/Raum) $isBuilding = (!empty($obj->type_system_code) && $obj->type_system_code === 'GLOBAL'); + $nodeIsBuilding[(int)$obj->rowid] = $isBuilding; + $nodeIds[$obj->rowid] = true; + $rows[] = $obj; + } + $db->free($resql); + + // 2. Pass: Nodes und Hierarchie-Edges aufbauen + foreach ($rows as $obj) { + $isBuilding = $nodeIsBuilding[(int)$obj->rowid]; $nodeId = 'n_'.$obj->rowid; - $nodeIds[$obj->rowid] = true; $nodeData = array( 'id' => $nodeId, @@ -115,38 +132,64 @@ if ($resql) { ); // Feldwerte mit Metadaten (Label, Display-Modus, Badge-Farbe) + // Iteration über $fieldMeta (nach position sortiert), nicht über $rawValues (JSON-Reihenfolge) + // Aufteilen: fields = auf dem Node (show_in_tree=1), hover_fields = im Tooltip (show_in_hover=1) if (!empty($obj->field_values)) { $rawValues = json_decode($obj->field_values, true); if (is_array($rawValues) && !empty($rawValues)) { $typeId = (int) $obj->fk_anlage_type; $meta = isset($fieldMeta[$typeId]) ? $fieldMeta[$typeId] : array(); - $fields = array(); - foreach ($rawValues as $code => $val) { + $treeFields = array(); + $hoverFields = array(); + foreach ($meta as $code => $fm) { + $val = isset($rawValues[$code]) ? $rawValues[$code] : null; if ($val === '' || $val === null) continue; - $fm = isset($meta[$code]) ? $meta[$code] : array(); - $display = isset($fm['display']) ? $fm['display'] : 'badge'; - // Versteckte Felder überspringen - if ($display === 'none') continue; - $label = isset($fm['label']) ? $fm['label'] : $code; // Checkbox-Werte anpassen - if (isset($fm['type']) && $fm['type'] === 'checkbox') { + if ($fm['type'] === 'checkbox') { $val = $val ? '1' : '0'; } - $fields[] = array( - 'label' => $label, + $fieldEntry = array( + 'label' => $fm['label'], 'value' => $val, - 'display' => $display, - 'color' => isset($fm['color']) ? $fm['color'] : '', - 'type' => isset($fm['type']) ? $fm['type'] : 'text', + 'display' => $fm['display'], + 'color' => $fm['color'], + 'type' => $fm['type'], ); + // Auf dem Node: nur Felder mit show_in_tree=1 + if (!empty($fm['show_in_tree'])) { + $treeFields[] = $fieldEntry; + } + // Im Tooltip: nur Felder mit show_in_hover=1 + if (!empty($fm['show_in_hover'])) { + $hoverFields[] = $fieldEntry; + } } - $nodeData['fields'] = $fields; + $nodeData['fields'] = $treeFields; + $nodeData['hover_fields'] = $hoverFields; } } - // Compound-Parent aus fk_parent (Eltern-Kind-Verschachtelung) - if ($obj->fk_parent > 0) { - $nodeData['parent'] = 'n_'.$obj->fk_parent; + // Compound-Parent: NUR wenn Eltern-Node ein Gebäude/Raum ist + // 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) + $hierKey = min($parentId, (int)$obj->rowid).'_'.max($parentId, (int)$obj->rowid); + $hierarchyEdges[$hierKey] = array( + 'data' => array( + 'id' => 'hier_'.$parentId.'_'.$obj->rowid, + 'source' => 'n_'.$parentId, + 'target' => 'n_'.$obj->rowid, + 'is_hierarchy' => true, + ), + 'classes' => 'hierarchy-edge' + ); + } } $elements['nodes'][] = array( @@ -154,7 +197,6 @@ if ($resql) { 'classes' => $isBuilding ? 'building-node' : 'device-node' ); } - $db->free($resql); } // Verbindungen laden @@ -164,6 +206,9 @@ $connections = $connObj->fetchBySociete($socId, 0); // Verwendete Kabeltypen für die Legende sammeln $usedCableTypes = array(); +// Kabel-Paare merken (um Hierarchie-Kanten zu ersetzen) +$cableConnectedPairs = array(); + foreach ($connections as $conn) { // Nur Edges für tatsächlich geladene Nodes if (!isset($nodeIds[$conn->fk_source]) || !isset($nodeIds[$conn->fk_target])) { @@ -207,6 +252,14 @@ foreach ($connections as $conn) { } } + // Echte Kabelverbindung (nicht durchgeschleift) → Hierarchie-Kante überflüssig + if (!$isPassthrough) { + $src = (int) $conn->fk_source; + $tgt = (int) $conn->fk_target; + $pairKey = min($src, $tgt).'_'.max($src, $tgt); + $cableConnectedPairs[$pairKey] = true; + } + $elements['edges'][] = array( 'data' => array( 'id' => 'conn_'.$conn->id, @@ -224,6 +277,13 @@ foreach ($connections as $conn) { ); } +// Hierarchie-Kanten hinzufügen, aber nur wenn kein echtes Kabel zwischen den Geräten existiert +foreach ($hierarchyEdges as $hierKey => $hierEdge) { + if (!isset($cableConnectedPairs[$hierKey])) { + $elements['edges'][] = $hierEdge; + } +} + // Prüfen ob gespeicherte Positionen vorhanden sind $hasPositions = false; foreach ($elements['nodes'] as $node) { diff --git a/ajax/graph_save_positions.php b/ajax/graph_save_positions.php old mode 100644 new mode 100755 diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php index 41bcb13..fc2e795 100755 --- a/core/modules/modKundenKarte.class.php +++ b/core/modules/modKundenKarte.class.php @@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '5.0.0'; + $this->version = '5.1.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/css/kundenkarte_cytoscape.css b/css/kundenkarte_cytoscape.css old mode 100644 new mode 100755 index 3c1f6e9..cfe401f --- a/css/kundenkarte_cytoscape.css +++ b/css/kundenkarte_cytoscape.css @@ -134,6 +134,12 @@ height: 0; } +.kundenkarte-graph-legend-line.hierarchy { + background: none; + border-top: 2px dotted #6a7a8a; + height: 0; +} + .kundenkarte-graph-legend-box { width: 16px; height: 12px; @@ -177,46 +183,109 @@ position: absolute; z-index: 100; background: var(--colorbacktabcard1, #1e2a3a); - border: 1px solid var(--inputbordercolor, #3a6a8e); - border-radius: 6px; - padding: 10px 14px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0; font-size: 12px; color: var(--colortext, #ccc); - max-width: 300px; + max-width: 320px; + min-width: 200px; pointer-events: none; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +.kundenkarte-graph-tooltip .tooltip-header { + padding: 10px 14px; + background: rgba(255, 255, 255, 0.03); + margin-bottom: 0; } .kundenkarte-graph-tooltip .tooltip-title { font-weight: bold; color: var(--colortextlink, #7ab0d4); - margin-bottom: 4px; font-size: 13px; + line-height: 1.3; +} + +.kundenkarte-graph-tooltip .tooltip-title i { + margin-right: 4px; + opacity: 0.8; } .kundenkarte-graph-tooltip .tooltip-type { - color: var(--colortext, #888); + color: var(--colortext, #999); font-size: 11px; - margin-bottom: 6px; + margin-top: 3px; opacity: 0.7; } +.kundenkarte-graph-tooltip .tooltip-system { + background: rgba(255, 255, 255, 0.08); + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + margin-left: 4px; +} + +.kundenkarte-graph-tooltip .tooltip-fields { + padding: 6px 14px; +} + .kundenkarte-graph-tooltip .tooltip-field { display: flex; justify-content: space-between; - gap: 10px; - padding: 2px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + align-items: baseline; + gap: 12px; + padding: 3px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.kundenkarte-graph-tooltip .tooltip-field:last-child { + border-bottom: none; } .kundenkarte-graph-tooltip .tooltip-field-label { color: var(--colortext, #888); - opacity: 0.7; + opacity: 0.6; + font-size: 11px; + white-space: nowrap; } .kundenkarte-graph-tooltip .tooltip-field-value { - color: var(--colortext, #ddd); + color: var(--colortext, #eee); text-align: right; + font-weight: 500; +} + +.kundenkarte-graph-tooltip .tooltip-field-badge { + color: #fff; + text-align: right; + font-weight: 600; + font-size: 11px; + padding: 1px 8px; + border-radius: 4px; + white-space: nowrap; +} + +.kundenkarte-graph-tooltip .tooltip-footer { + padding: 6px 14px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + display: flex; + gap: 10px; + font-size: 11px; + color: var(--colortext, #888); + opacity: 0.7; +} + +.kundenkarte-graph-tooltip .tooltip-file-badge { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.kundenkarte-graph-tooltip .tooltip-file-badge i { + font-size: 10px; } /* Leer-Zustand */ diff --git a/js/cose-base.js b/js/cose-base.js old mode 100644 new mode 100755 diff --git a/js/cytoscape-cose-bilkent.js b/js/cytoscape-cose-bilkent.js old mode 100644 new mode 100755 diff --git a/js/cytoscape-dagre.js b/js/cytoscape-dagre.js old mode 100644 new mode 100755 diff --git a/js/cytoscape.min.js b/js/cytoscape.min.js old mode 100644 new mode 100755 diff --git a/js/dagre.min.js b/js/dagre.min.js old mode 100644 new mode 100755 diff --git a/js/kundenkarte_cytoscape.js b/js/kundenkarte_cytoscape.js old mode 100644 new mode 100755 index f5c4ef9..a600ddd --- a/js/kundenkarte_cytoscape.js +++ b/js/kundenkarte_cytoscape.js @@ -203,6 +203,10 @@ html += ''; html += ' Durchgeschleift'; + // Hierarchie (Eltern→Kind Beziehung zwischen Geräten) + html += ''; + html += ' Hierarchie'; + $legend.html(html); }, @@ -281,7 +285,7 @@ ? namePart + ' (' + parens.join(', ') + ')' : namePart; } else { - // Gerät: Name (+ Klammer-Felder) + Trennlinie + Badge-Felder + // Gerät: Name (+ Klammer) + Typ + Trennlinie + Badge-Werte mit Feldbezeichnung var parenParts = []; var badgeLines = []; @@ -295,6 +299,7 @@ if (f.display === 'parentheses') { parenParts.push(v); } else if (f.display === 'badge') { + // Feldname: Wert badgeLines.push(f.label + ': ' + v); } } @@ -307,12 +312,18 @@ } else { lines.push(namePart); } - // Badge-Felder als Karten-Zeilen - if (badgeLines.length > 0) { - lines.push('─────────────'); - for (var j = 0; j < badgeLines.length; j++) { - lines.push(badgeLines[j]); - } + // Trennlinie + Typ + Badge-Werte + var hasDetails = (n.data.type_label || badgeLines.length > 0); + if (hasDetails) { + lines.push('────────────────────'); + } + // Typ-Bezeichnung immer unter dem Strich + if (n.data.type_label) { + lines.push('Typ: ' + n.data.type_label); + } + // Badge-Werte mit Feldbezeichnung + for (var j = 0; j < badgeLines.length; j++) { + lines.push(badgeLines[j]); } // Datei-Indikatoren @@ -320,8 +331,8 @@ if (n.data.image_count > 0) fileInfo.push('\ud83d\uddbc ' + n.data.image_count); if (n.data.doc_count > 0) fileInfo.push('\ud83d\udcc4 ' + n.data.doc_count); if (fileInfo.length > 0) { - if (badgeLines.length === 0) lines.push('─────────────'); - lines.push(fileInfo.join(' ')); + if (badgeLines.length === 0) lines.push('────────────────────'); + lines.push(fileInfo.join(' \u00b7 ')); } n.data.display_label = lines.join('\n'); @@ -453,31 +464,32 @@ 'min-height': '60px' } }, - // Geräte - Karten-Design mit Feldwerten + // Geräte - Karten-Design mit Feldwerten (kompakt) { selector: '.device-node', style: { 'shape': 'roundrectangle', 'width': 'label', 'height': 'label', - 'padding': '14px', - 'background-color': '#2d4a3a', + 'padding': '12px', + 'background-color': '#2d3d35', + 'background-opacity': 0.95, 'border-width': 2, 'border-color': function(node) { return node.data('type_color') || '#5a9a6a'; }, 'label': 'data(display_label)', 'font-family': faFont, - 'font-weight': 'bold', + 'font-weight': 900, 'text-valign': 'center', 'text-halign': 'center', 'text-justification': 'left', 'font-size': '11px', 'color': textColor, 'text-wrap': 'wrap', - 'text-max-width': '220px', - 'line-height': 1.4 + 'text-max-width': '240px', + 'line-height': 1.5 } }, - // Kabel - Farbe aus medium_color, Fallback grün + // Kabel - Farbe aus medium_color, Fallback grün, rechtwinklig { selector: '.cable-edge', style: { @@ -487,7 +499,9 @@ }, 'target-arrow-shape': 'none', 'source-arrow-shape': 'none', - 'curve-style': 'bezier', + 'curve-style': 'taxi', + 'taxi-direction': 'downward', + 'taxi-turn': '40px', 'label': 'data(label)', 'font-size': '9px', 'color': '#8a9aa8', @@ -510,6 +524,21 @@ 'curve-style': 'bezier' } }, + // Hierarchie-Kanten (Gerät→Gerät Eltern-Kind, rechtwinklig) + { + selector: '.hierarchy-edge', + style: { + 'width': 1.5, + 'line-color': '#6a7a8a', + 'line-style': 'dotted', + 'target-arrow-shape': 'none', + 'source-arrow-shape': 'none', + 'curve-style': 'taxi', + 'taxi-direction': 'downward', + 'taxi-turn': '30px', + 'opacity': 0.6 + } + }, // Hover { selector: 'node:active', @@ -999,35 +1028,45 @@ */ showNodeTooltip: function(node, position) { var data = node.data(); + + // Header: Typ-Farbe als Akzentlinie + var accentColor = data.type_color || '#5a9a6a'; + var html = '