Version 5.1.0 - Graph-Feldanzeige & Shared Library

- Felder nach position sortiert (nicht JSON-Reihenfolge)
- show_in_tree/show_in_hover Filterung auf Graph-Nodes und Tooltip
- Badge-Werte im Graph mit Feldbezeichnung (Label: Wert)
- Tooltip: Farbige Badge-Kaesten, Typ/System entfernt (redundant)
- Shared Library lib/graph_view.lib.php (Toolbar, Container, Legende)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-22 20:20:39 +01:00
parent e6b28fe85e
commit f7f84228ad
15 changed files with 473 additions and 186 deletions

View file

@ -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

104
ajax/graph_data.php Normal file → Executable file
View file

@ -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;
}
$nodeData['fields'] = $fields;
// Im Tooltip: nur Felder mit show_in_hover=1
if (!empty($fm['show_in_hover'])) {
$hoverFields[] = $fieldEntry;
}
}
$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) {

0
ajax/graph_save_positions.php Normal file → Executable file
View file

View file

@ -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';

95
css/kundenkarte_cytoscape.css Normal file → Executable file
View file

@ -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 */

0
js/cose-base.js Normal file → Executable file
View file

0
js/cytoscape-cose-bilkent.js Normal file → Executable file
View file

0
js/cytoscape-dagre.js Normal file → Executable file
View file

0
js/cytoscape.min.js vendored Normal file → Executable file
View file

0
js/dagre.min.js vendored Normal file → Executable file
View file

97
js/kundenkarte_cytoscape.js Normal file → Executable file
View file

@ -203,6 +203,10 @@
html += '<span class="kundenkarte-graph-legend-item">';
html += '<span class="kundenkarte-graph-legend-line passthrough"></span> Durchgeschleift</span>';
// Hierarchie (Eltern→Kind Beziehung zwischen Geräten)
html += '<span class="kundenkarte-graph-legend-item">';
html += '<span class="kundenkarte-graph-legend-line hierarchy"></span> Hierarchie</span>';
$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,21 +312,27 @@
} else {
lines.push(namePart);
}
// Badge-Felder als Karten-Zeilen
if (badgeLines.length > 0) {
lines.push('─────────────');
// 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
var fileInfo = [];
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 = '<div class="tooltip-header" style="border-left:3px solid ' + accentColor + ';padding-left:10px;">';
// Überschrift: Icon + Bezeichnung
var html = '<div class="tooltip-title">';
html += '<div class="tooltip-title">';
if (data.type_picto) {
html += '<i class="fa ' + this.escapeHtml(data.type_picto) + '"></i> ';
}
html += this.escapeHtml(data.label) + '</div>';
html += '</div>';
if (data.fields && data.fields.length > 0) {
for (var i = 0; i < data.fields.length && i < 10; i++) {
var f = data.fields[i];
// Hover-Felder anzeigen (nach Position sortiert, vom Backend gefiltert)
var hoverFields = data.hover_fields || [];
if (hoverFields.length > 0) {
html += '<div class="tooltip-fields">';
for (var i = 0; i < hoverFields.length; i++) {
var f = hoverFields[i];
if (!f.value || f.value === '') continue;
var val = f.value;
if (f.type === 'checkbox' && (val === '1' || val === 'true')) val = '\u2713';
if (f.type === 'checkbox' && (val === '0' || val === 'false')) val = '\u2717';
html += '<div class="tooltip-field">';
html += '<span class="tooltip-field-label">' + this.escapeHtml(f.label) + '</span>';
if (f.display === 'badge') {
var bgColor = f.color || '#4a5568';
html += '<span class="tooltip-field-value" style="background:' + bgColor + ';padding:1px 6px;border-radius:3px;color:#fff;">' + this.escapeHtml(String(val)) + '</span>';
if (f.color) {
html += '<span class="tooltip-field-badge" style="background:' + f.color + ';">' + this.escapeHtml(String(val)) + '</span>';
} else {
html += '<span class="tooltip-field-value">' + this.escapeHtml(String(val)) + '</span>';
}
html += '</div>';
}
html += '</div>';
}
// Datei-Info am unteren Rand
if (data.image_count > 0 || data.doc_count > 0) {
html += '<div class="tooltip-type" style="margin-top:4px;">';
if (data.image_count > 0) html += '<i class="fa fa-image"></i> ' + data.image_count + ' ';
if (data.doc_count > 0) html += '<i class="fa fa-file"></i> ' + data.doc_count;
html += '<div class="tooltip-footer">';
if (data.image_count > 0) html += '<span class="tooltip-file-badge"><i class="fa fa-image"></i> ' + data.image_count + '</span>';
if (data.doc_count > 0) html += '<span class="tooltip-file-badge"><i class="fa fa-file-text-o"></i> ' + data.doc_count + '</span>';
html += '</div>';
}
@ -1041,7 +1080,12 @@
*/
showEdgeTooltip: function(edge, position) {
var data = edge.data();
var html = '<div class="tooltip-title">Verbindung</div>';
// Farbe der Verbindung als Akzent
var edgeColor = data.medium_color || '#5a8a5a';
var html = '<div class="tooltip-header" style="border-left:3px solid ' + edgeColor + ';padding-left:10px;">';
html += '<div class="tooltip-title"><i class="fa fa-plug"></i> Verbindung</div>';
html += '</div>';
html += '<div class="tooltip-fields">';
if (data.medium_type) {
html += '<div class="tooltip-field"><span class="tooltip-field-label">Typ</span><span class="tooltip-field-value">' + this.escapeHtml(data.medium_type) + '</span></div>';
}
@ -1051,6 +1095,7 @@
if (data.medium_length) {
html += '<div class="tooltip-field"><span class="tooltip-field-label">Länge</span><span class="tooltip-field-value">' + this.escapeHtml(data.medium_length) + '</span></div>';
}
html += '</div>';
this.tooltipEl.innerHTML = html;
this.tooltipEl.style.display = 'block';
this.positionTooltip(position);

0
js/layout-base.js Normal file → Executable file
View file

160
lib/graph_view.lib.php Normal file
View file

@ -0,0 +1,160 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Gemeinsame Graph-Ansicht Funktionen für anlagen.php und contact_anlagen.php
* Vermeidet doppelten Code für Toolbar, Container, Kontextmenü, Legende
*/
/**
* Graph-spezifische JS/CSS-Dateien zu den Asset-Arrays hinzufügen
*
* @param array $jsFiles Referenz auf JS-Array
* @param array $cssFiles Referenz auf CSS-Array
* @param string $viewMode 'tree' oder 'graph'
*/
function kundenkarte_graph_add_assets(&$jsFiles, &$cssFiles, $viewMode)
{
if ($viewMode === 'graph') {
$jsFiles[] = '/kundenkarte/js/dagre.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape-dagre.js';
$jsFiles[] = '/kundenkarte/js/kundenkarte_cytoscape.js?v='.time();
$cssFiles[] = '/kundenkarte/css/kundenkarte_cytoscape.css?v='.time();
} else {
array_unshift($jsFiles, '/kundenkarte/js/pathfinding.min.js');
}
}
/**
* Graph-Toolbar rendern (2 Zeilen: Aktionen + Steuerung)
* Wird UNTER der System-Tab-Borderlinie ausgegeben
*
* @param array $params Konfiguration:
* 'socid' int Kunden-ID
* 'contactid' int Kontakt-ID (0 für Kundenansicht)
* 'systemid' int Aktuelles System
* 'viewMode' string 'tree' oder 'graph'
* 'permissiontoadd' bool Schreibberechtigung
* 'pageUrl' string Aktuelle Seiten-URL ($_SERVER['PHP_SELF'])
*/
function kundenkarte_graph_print_toolbar($params)
{
global $langs;
$socId = (int) ($params['socid'] ?? 0);
$contactId = (int) ($params['contactid'] ?? 0);
$systemId = (int) ($params['systemid'] ?? 0);
$viewMode = $params['viewMode'] ?? 'tree';
$permissiontoadd = !empty($params['permissiontoadd']);
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
// View-Toggle URL: ID-Parameter je nach Kontext
$idParam = ($contactId > 0) ? $contactId : $socId;
$toggleView = ($viewMode === 'graph') ? 'tree' : 'graph';
$toggleUrl = $pageUrl.'?id='.$idParam.'&system='.$systemId.'&view='.$toggleView;
$toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap';
$toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView');
// Connection-URL: bei Kontakten wird socid + contactid übergeben
$connUrlParams = 'socid='.$socId;
if ($contactId > 0) {
$connUrlParams .= '&contactid='.$contactId;
}
print '<div class="kundenkarte-graph-toolbar">';
// Zeile 1: Ansicht-Wechsel + Aktionen
print '<div class="kundenkarte-graph-toolbar-row">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>';
if ($permissiontoadd) {
print '<a class="button small" href="'.$pageUrl.'?id='.$idParam.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?'.$connUrlParams.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
}
print '</div>';
// Zeile 2: Graph-Steuerung (Anordnen rechts)
print '<div class="kundenkarte-graph-toolbar-row">';
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-refresh"></i> Layout</button>';
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom"><i class="fa fa-arrows"></i> Scroll</button>';
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Vergrößern"><i class="fa fa-search-plus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Verkleinern"><i class="fa fa-search-minus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-fit" title="Einpassen"><i class="fa fa-crosshairs"></i> Fit</button>';
print '<button type="button" class="button small" id="btn-graph-export-png" title="PNG exportieren"><i class="fa fa-download"></i> PNG</button>';
if ($permissiontoadd) {
print '<span class="kundenkarte-graph-toolbar-spacer"></span>';
print '<button type="button" class="button small" id="btn-graph-edit-mode" title="Elemente anordnen"><i class="fa fa-hand-paper-o"></i> Anordnen</button>';
print '<button type="button" class="button small btn-graph-save" id="btn-graph-save-positions" title="Positionen speichern" style="display:none;"><i class="fa fa-check"></i> Speichern</button>';
print '<button type="button" class="button small btn-graph-cancel" id="btn-graph-cancel-edit" title="Abbrechen" style="display:none;"><i class="fa fa-times"></i> Abbrechen</button>';
}
print '</div>';
print '</div>';
}
/**
* Graph-Container, Kontextmenü und Legende rendern
*
* @param array $params Konfiguration:
* 'socid' int Kunden-ID
* 'contactid' int Kontakt-ID (0 für Kundenansicht)
* 'systemid' int Aktuelles System
* 'permissiontoadd' bool Schreibberechtigung
* 'permissiontodelete' bool Löschberechtigung
* 'pageUrl' string Aktuelle Seiten-URL
*/
function kundenkarte_graph_print_container($params)
{
global $langs;
$socId = (int) ($params['socid'] ?? 0);
$contactId = (int) ($params['contactid'] ?? 0);
$systemId = (int) ($params['systemid'] ?? 0);
$permissiontoadd = !empty($params['permissiontoadd']);
$permissiontodelete = !empty($params['permissiontodelete']);
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
$graphAjaxUrl = dol_buildpath('/kundenkarte/ajax/graph_data.php', 1);
$graphSaveUrl = dol_buildpath('/kundenkarte/ajax/graph_save_positions.php', 1);
$graphModuleUrl = dol_buildpath('/kundenkarte', 1);
print '<div class="kundenkarte-graph-wrapper">';
// Suchfeld als Overlay
print '<div class="kundenkarte-graph-search-floating">';
print '<input type="text" id="kundenkarte-graph-search" placeholder="'.$langs->trans('SearchPlaceholder').'" autocomplete="off">';
print '</div>';
// Graph-Container mit Data-Attributen für JS-Initialisierung
print '<div id="kundenkarte-graph-container"';
print ' data-ajax-url="'.dol_escape_htmltag($graphAjaxUrl).'"';
print ' data-save-url="'.dol_escape_htmltag($graphSaveUrl).'"';
print ' data-module-url="'.dol_escape_htmltag($graphModuleUrl).'"';
print ' data-socid="'.$socId.'"';
if ($contactId > 0) {
print ' data-contactid="'.$contactId.'"';
}
print ' data-systemid="'.$systemId.'"';
print ' data-can-edit="'.($permissiontoadd ? '1' : '0').'"';
print ' data-can-delete="'.($permissiontodelete ? '1' : '0').'"';
print ' data-page-url="'.dol_escape_htmltag($pageUrl).'"';
print '>';
print '<div class="kundenkarte-graph-loading"><i class="fa fa-spinner fa-spin"></i> '.$langs->trans('GraphLoading').'</div>';
print '</div>';
// Kontextmenü (Rechtsklick auf Node)
print '<div id="kundenkarte-graph-contextmenu" class="kundenkarte-graph-contextmenu" style="display:none;">';
print '<a class="ctx-item ctx-view" data-action="view"><i class="fa fa-eye"></i> '.$langs->trans('View').'</a>';
if ($permissiontoadd) {
print '<a class="ctx-item ctx-add-child" data-action="add-child"><i class="fa fa-plus"></i> '.$langs->trans('AddChild').'</a>';
print '<a class="ctx-item ctx-edit" data-action="edit"><i class="fa fa-edit"></i> '.$langs->trans('Edit').'</a>';
print '<a class="ctx-item ctx-copy" data-action="copy"><i class="fa fa-copy"></i> '.$langs->trans('Copy').'</a>';
}
if ($permissiontodelete) {
print '<a class="ctx-item ctx-delete" data-action="delete"><i class="fa fa-trash"></i> '.$langs->trans('Delete').'</a>';
}
print '</div>';
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
print '<div id="kundenkarte-graph-legend" class="kundenkarte-graph-legend"></div>';
print '</div>'; // End kundenkarte-graph-wrapper
}

View file

@ -373,18 +373,11 @@ if (!in_array($viewMode, array('tree', 'graph'))) {
$viewMode = $defaultView;
}
dol_include_once('/kundenkarte/lib/graph_view.lib.php');
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
if ($viewMode === 'graph') {
$jsFiles[] = '/kundenkarte/js/dagre.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape-dagre.js';
$jsFiles[] = '/kundenkarte/js/kundenkarte_cytoscape.js?v='.time();
$cssFiles[] = '/kundenkarte/css/kundenkarte_cytoscape.css?v='.time();
} else {
array_unshift($jsFiles, '/kundenkarte/js/pathfinding.min.js');
}
kundenkarte_graph_add_assets($jsFiles, $cssFiles, $viewMode);
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
@ -507,31 +500,14 @@ print '</div>'; // End kundenkarte-system-tabs-wrapper
// Graph-Toolbar: UNTER der System-Tab-Borderlinie
if ($isTreeView && $viewMode === 'graph') {
print '<div class="kundenkarte-graph-toolbar">';
// Zeile 1: Ansicht-Wechsel + Aktionen
print '<div class="kundenkarte-graph-toolbar-row">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>';
if ($user->hasRight('kundenkarte', 'write')) {
print '<a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?socid='.$id.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
}
print '</div>';
// Zeile 2: Graph-Steuerung (Anordnen rechts)
print '<div class="kundenkarte-graph-toolbar-row">';
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-refresh"></i> Layout</button>';
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom"><i class="fa fa-arrows"></i> Scroll</button>';
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Vergrößern"><i class="fa fa-search-plus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Verkleinern"><i class="fa fa-search-minus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-fit" title="Einpassen"><i class="fa fa-crosshairs"></i> Fit</button>';
print '<button type="button" class="button small" id="btn-graph-export-png" title="PNG exportieren"><i class="fa fa-download"></i> PNG</button>';
if ($permissiontoadd) {
print '<span class="kundenkarte-graph-toolbar-spacer"></span>';
print '<button type="button" class="button small" id="btn-graph-edit-mode" title="Elemente anordnen"><i class="fa fa-hand-paper-o"></i> Anordnen</button>';
print '<button type="button" class="button small btn-graph-save" id="btn-graph-save-positions" title="Positionen speichern" style="display:none;"><i class="fa fa-check"></i> Speichern</button>';
print '<button type="button" class="button small btn-graph-cancel" id="btn-graph-cancel-edit" title="Abbrechen" style="display:none;"><i class="fa fa-times"></i> Abbrechen</button>';
}
print '</div>';
print '</div>';
kundenkarte_graph_print_toolbar(array(
'socid' => $id,
'contactid' => 0,
'systemid' => $systemId,
'viewMode' => $viewMode,
'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'],
));
}
// Add system form (hidden by default)
@ -1062,43 +1038,14 @@ if (empty($customerSystems)) {
if ($viewMode === 'graph' && $isTreeView) {
// Graph-Ansicht: Container rendern, Daten werden per AJAX geladen
$graphAjaxUrl = dol_buildpath('/kundenkarte/ajax/graph_data.php', 1);
$graphSaveUrl = dol_buildpath('/kundenkarte/ajax/graph_save_positions.php', 1);
$graphModuleUrl = dol_buildpath('/kundenkarte', 1);
print '<div class="kundenkarte-graph-wrapper">';
print '<div class="kundenkarte-graph-search-floating">';
print '<input type="text" id="kundenkarte-graph-search" placeholder="'.$langs->trans('SearchPlaceholder').'" autocomplete="off">';
print '</div>';
print '<div id="kundenkarte-graph-container"';
print ' data-ajax-url="'.dol_escape_htmltag($graphAjaxUrl).'"';
print ' data-save-url="'.dol_escape_htmltag($graphSaveUrl).'"';
print ' data-module-url="'.dol_escape_htmltag($graphModuleUrl).'"';
print ' data-socid="'.$id.'"';
print ' data-systemid="'.$systemId.'"';
print ' data-can-edit="'.($permissiontoadd ? '1' : '0').'"';
print ' data-can-delete="'.($permissiontodelete ? '1' : '0').'"';
print ' data-page-url="'.dol_escape_htmltag($_SERVER['PHP_SELF']).'"';
print '>';
print '<div class="kundenkarte-graph-loading"><i class="fa fa-spinner fa-spin"></i> '.$langs->trans('GraphLoading').'</div>';
print '</div>';
// Kontextmenü (Rechtsklick auf Node)
print '<div id="kundenkarte-graph-contextmenu" class="kundenkarte-graph-contextmenu" style="display:none;">';
print '<a class="ctx-item ctx-view" data-action="view"><i class="fa fa-eye"></i> '.$langs->trans('View').'</a>';
if ($permissiontoadd) {
print '<a class="ctx-item ctx-add-child" data-action="add-child"><i class="fa fa-plus"></i> '.$langs->trans('AddChild').'</a>';
print '<a class="ctx-item ctx-edit" data-action="edit"><i class="fa fa-edit"></i> '.$langs->trans('Edit').'</a>';
print '<a class="ctx-item ctx-copy" data-action="copy"><i class="fa fa-copy"></i> '.$langs->trans('Copy').'</a>';
}
if ($permissiontodelete) {
print '<a class="ctx-item ctx-delete" data-action="delete"><i class="fa fa-trash"></i> '.$langs->trans('Delete').'</a>';
}
print '</div>';
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
print '<div id="kundenkarte-graph-legend" class="kundenkarte-graph-legend"></div>';
print '</div>';
kundenkarte_graph_print_container(array(
'socid' => $id,
'contactid' => 0,
'systemid' => $systemId,
'permissiontoadd' => $permissiontoadd,
'permissiontodelete' => $permissiontodelete,
'pageUrl' => $_SERVER['PHP_SELF'],
));
} else {
// Baumansicht (klassisch)

View file

@ -365,21 +365,17 @@ if ($action == 'togglepin' && $permissiontoadd) {
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs);
// Ansichtsmodus (Admin-Setting)
$viewMode = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
// Ansichtsmodus: URL-Parameter hat Vorrang, sonst Admin-Setting
dol_include_once('/kundenkarte/lib/graph_view.lib.php');
$defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
$viewMode = GETPOST('view', 'aZ09');
if (!in_array($viewMode, array('tree', 'graph'))) {
$viewMode = $defaultView;
}
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
if ($viewMode === 'graph') {
$jsFiles[] = '/kundenkarte/js/dagre.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape.min.js';
$jsFiles[] = '/kundenkarte/js/cytoscape-dagre.js';
$jsFiles[] = '/kundenkarte/js/kundenkarte_cytoscape.js?v='.time();
$cssFiles[] = '/kundenkarte/css/kundenkarte_cytoscape.css?v='.time();
} else {
array_unshift($jsFiles, '/kundenkarte/js/pathfinding.min.js');
}
kundenkarte_graph_add_assets($jsFiles, $cssFiles, $viewMode);
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
@ -465,29 +461,20 @@ if ($permissiontoadd) {
}
print '</div>';
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
// Steuerungs-Buttons (nur wenn kein Formular aktiv)
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
if ($viewMode === 'graph') {
// Graph Controls: Aktionen + Zoom
print '<div class="kundenkarte-graph-toolbar">';
if ($user->hasRight('kundenkarte', 'write')) {
print '<div class="kundenkarte-graph-actions">';
print '<a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?socid='.$object->socid.'&contactid='.$id.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
print '</div>';
}
print '<div class="kundenkarte-graph-zoom-controls">';
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-undo"></i></button>';
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom ein"><i class="fa fa-mouse-pointer"></i></button>';
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Zoom +"><i class="fa fa-search-plus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Zoom -"><i class="fa fa-search-minus"></i></button>';
print '<button type="button" class="button small" id="btn-graph-fit" title="Standard-Zoom"><i class="fa fa-crosshairs"></i></button>';
print '</div>';
print '</div>';
} else {
$toggleView = ($viewMode === 'graph') ? 'tree' : 'graph';
$toggleUrl = $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&view='.$toggleView;
$toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap';
$toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView');
if ($viewMode !== 'graph') {
// Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper)
print '<div class="kundenkarte-tree-controls">';
// Compact mode toggle (visible on mobile)
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel;
print '</a>';
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
@ -509,6 +496,18 @@ if ($isTreeView) {
print '</div>'; // End kundenkarte-system-tabs-wrapper
// Graph-Toolbar: UNTER der System-Tab-Borderlinie
if ($isTreeView && $viewMode === 'graph') {
kundenkarte_graph_print_toolbar(array(
'socid' => $object->socid,
'contactid' => $id,
'systemid' => $systemId,
'viewMode' => $viewMode,
'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'],
));
}
// Add system form (hidden by default)
if ($permissiontoadd && !empty($availableSystems)) {
print '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
@ -1037,24 +1036,14 @@ if (empty($customerSystems)) {
if ($viewMode === 'graph' && $isTreeView) {
// Graph-Ansicht: Container rendern, Daten werden per AJAX geladen
$graphAjaxUrl = dol_buildpath('/kundenkarte/ajax/graph_data.php', 1);
$graphSaveUrl = dol_buildpath('/kundenkarte/ajax/graph_save_positions.php', 1);
$graphModuleUrl = dol_buildpath('/kundenkarte', 1);
print '<div class="kundenkarte-graph-wrapper">';
print '<div id="kundenkarte-graph-container"';
print ' data-ajax-url="'.dol_escape_htmltag($graphAjaxUrl).'"';
print ' data-save-url="'.dol_escape_htmltag($graphSaveUrl).'"';
print ' data-module-url="'.dol_escape_htmltag($graphModuleUrl).'"';
print ' data-socid="'.$object->socid.'"';
print ' data-contactid="'.$id.'"';
print '>';
print '<div class="kundenkarte-graph-loading"><i class="fa fa-spinner fa-spin"></i> '.$langs->trans('GraphLoading').'</div>';
print '</div>';
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
print '<div id="kundenkarte-graph-legend" class="kundenkarte-graph-legend"></div>';
print '</div>';
kundenkarte_graph_print_container(array(
'socid' => $object->socid,
'contactid' => $id,
'systemid' => $systemId,
'permissiontoadd' => $permissiontoadd,
'permissiontodelete' => $permissiontodelete,
'pageUrl' => $_SERVER['PHP_SELF'],
));
} else {
// Baumansicht (klassisch)