feat(graph): View-Modes pro System, Compound-Node Fixes, Resize-Handles, Admin-Gear

- Ansichtsmodus (tree/graph/both) pro System konfigurierbar
- Admin-Zahnrad-Icon auf Kunden- und Kontakt-Anlagen-Tab
- Compound-Nodes: Alle Nachkommen eines Gebäudes werden umschlossen
- Leitungen/Verbindungen aus der Baumansicht entfernt (nur noch im Graph)
- Resize-Handles für Gebäude-Nodes im Bearbeitungsmodus
- graph_width/graph_height Spalten für persistente Gebäudegrößen
- view_modes Spalte in System-Tabelle
- DB-Migrationen in modKundenKarte

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-02 20:33:47 +01:00
parent e269584396
commit ee4c6688d9
12 changed files with 442 additions and 54 deletions

View file

@ -36,6 +36,8 @@ if ($action == 'add') {
$picto = GETPOST('picto', 'alphanohtml'); $picto = GETPOST('picto', 'alphanohtml');
$color = GETPOST('color', 'alphanohtml'); $color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position'); $position = GETPOSTINT('position');
$viewModes = GETPOST('view_modes', 'aZ09');
if (!in_array($viewModes, array('tree', 'graph', 'both'))) $viewModes = 'both';
// Tree display config // Tree display config
$treeConfig = array( $treeConfig = array(
@ -54,12 +56,13 @@ if ($action == 'add') {
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors'); setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
} else { } else {
$sql = "INSERT INTO ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system"; $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 .= " VALUES ('".$db->escape(strtoupper($code))."', '".$db->escape($label)."',";
$sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").","; $sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").",";
$sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").","; $sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").",";
$sql .= " ".((int) $position).", 1, 0,"; $sql .= " ".((int) $position).", 1, 0,";
$sql .= " '".$db->escape($treeConfigJson)."')"; $sql .= " '".$db->escape($treeConfigJson)."',";
$sql .= " '".$db->escape($viewModes)."')";
$result = $db->query($sql); $result = $db->query($sql);
if ($result) { if ($result) {
@ -77,6 +80,8 @@ if ($action == 'update') {
$picto = GETPOST('picto', 'alphanohtml'); $picto = GETPOST('picto', 'alphanohtml');
$color = GETPOST('color', 'alphanohtml'); $color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position'); $position = GETPOSTINT('position');
$viewModes = GETPOST('view_modes', 'aZ09');
if (!in_array($viewModes, array('tree', 'graph', 'both'))) $viewModes = 'both';
// Tree display config // Tree display config
$treeConfig = array( $treeConfig = array(
@ -98,6 +103,7 @@ if ($action == 'update') {
$sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL"); $sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL");
$sql .= ", position = ".((int) $position); $sql .= ", position = ".((int) $position);
$sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'"; $sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'";
$sql .= ", view_modes = '".$db->escape($viewModes)."'";
$sql .= " WHERE rowid = ".((int) $systemId); $sql .= " WHERE rowid = ".((int) $systemId);
$result = $db->query($sql); $result = $db->query($sql);
@ -212,6 +218,21 @@ if ($action == 'create' || $action == 'edit') {
print '<tr><td>'.$langs->trans('Position').'</td>'; print '<tr><td>'.$langs->trans('Position').'</td>';
print '<td><input type="number" name="position" class="flat" value="'.($system ? $system->position : 0).'" min="0"></td></tr>'; print '<td><input type="number" name="position" class="flat" value="'.($system ? $system->position : 0).'" min="0"></td></tr>';
// Verfügbare Ansichten
$currentViewModes = ($system && !empty($system->view_modes)) ? $system->view_modes : 'both';
print '<tr><td>'.$langs->trans('ViewModes').'</td>';
print '<td><select name="view_modes" class="flat">';
$viewModesOptions = array(
'both' => $langs->trans('ViewModesBoth'),
'tree' => $langs->trans('ViewModesTreeOnly'),
'graph' => $langs->trans('ViewModesGraphOnly')
);
foreach ($viewModesOptions as $val => $lbl) {
$selected = ($currentViewModes == $val) ? ' selected' : '';
print '<option value="'.$val.'"'.$selected.'>'.$lbl.'</option>';
}
print '</select></td></tr>';
print '</table>'; print '</table>';
// Tree display configuration // Tree display configuration
@ -304,6 +325,7 @@ if ($action == 'create' || $action == 'edit') {
print '<th>'.$langs->trans('SystemCode').'</th>'; print '<th>'.$langs->trans('SystemCode').'</th>';
print '<th>'.$langs->trans('SystemLabel').'</th>'; print '<th>'.$langs->trans('SystemLabel').'</th>';
print '<th>'.$langs->trans('SystemPicto').'</th>'; print '<th>'.$langs->trans('SystemPicto').'</th>';
print '<th class="center">'.$langs->trans('ViewModes').'</th>';
print '<th class="center">'.$langs->trans('Position').'</th>'; print '<th class="center">'.$langs->trans('Position').'</th>';
print '<th class="center">'.$langs->trans('Status').'</th>'; print '<th class="center">'.$langs->trans('Status').'</th>';
print '<th class="center">'.$langs->trans('Actions').'</th>'; print '<th class="center">'.$langs->trans('Actions').'</th>';
@ -321,6 +343,10 @@ if ($action == 'create' || $action == 'edit') {
print dol_escape_htmltag($obj->picto); print dol_escape_htmltag($obj->picto);
} }
print '</td>'; print '</td>';
// Ansichtsmodus
$vmLabel = array('both' => 'Baum & Graph', 'tree' => 'Nur Baum', 'graph' => 'Nur Graph');
$vm = !empty($obj->view_modes) ? $obj->view_modes : 'both';
print '<td class="center">'.($vmLabel[$vm] ?? $vm).'</td>';
print '<td class="center">'.$obj->position.'</td>'; print '<td class="center">'.$obj->position.'</td>';
print '<td class="center">'; print '<td class="center">';

View file

@ -67,8 +67,9 @@ if ($resFields) {
// Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL) // Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL)
// Hierarchie kommt aus fk_parent (wie im Baum) // 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 = "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.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 .= " s.code as system_code, s.label as system_label,";
$sql .= " ts.code as type_system_code,"; $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,"; $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()); $elements = array('nodes' => array(), 'edges' => array());
$nodeIds = array(); $nodeIds = array();
// Zwischenspeicher: rowid → isBuilding (für Compound-Entscheidung)
$nodeIsBuilding = array(); $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 // Hierarchie-Kanten (Gerät→Gerät), werden durch echte Kabel ersetzt falls vorhanden
$hierarchyEdges = array(); $hierarchyEdges = array();
// Zwischenspeicher: alle DB-Zeilen für Zwei-Pass-Verarbeitung // Zwischenspeicher: alle DB-Zeilen für Zwei-Pass-Verarbeitung
@ -99,10 +101,11 @@ $rows = array();
$resql = $db->query($sql); $resql = $db->query($sql);
if ($resql) { 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)) { while ($obj = $db->fetch_object($resql)) {
$isBuilding = (!empty($obj->type_system_code) && $obj->type_system_code === 'GLOBAL'); $isBuilding = (!empty($obj->type_system_code) && $obj->type_system_code === 'GLOBAL');
$nodeIsBuilding[(int)$obj->rowid] = $isBuilding; $nodeIsBuilding[(int)$obj->rowid] = $isBuilding;
$nodeParentMap[(int)$obj->rowid] = (int)$obj->fk_parent;
$nodeIds[$obj->rowid] = true; $nodeIds[$obj->rowid] = true;
$rows[] = $obj; $rows[] = $obj;
} }
@ -129,6 +132,8 @@ if ($resql) {
'doc_count' => (int) $obj->doc_count, 'doc_count' => (int) $obj->doc_count,
'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null, 'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null,
'graph_y' => $obj->graph_y !== null ? (float) $obj->graph_y : 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) // 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) // Gerät→Gerät Hierarchie wird als Kante dargestellt (nicht verschachtelt)
$parentId = (int) $obj->fk_parent; $parentId = (int) $obj->fk_parent;
if ($parentId > 0 && isset($nodeIds[$parentId])) { if ($parentId > 0 && isset($nodeIds[$parentId])) {
$parentIsBuilding = !empty($nodeIsBuilding[$parentId]); // Hierarchie-Kante zum direkten Parent (wird ggf. durch Kabel ersetzt)
if ($parentIsBuilding) { if (!$isBuilding) {
// 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); $hierKey = min($parentId, (int)$obj->rowid).'_'.max($parentId, (int)$obj->rowid);
$hierarchyEdges[$hierKey] = array( $hierarchyEdges[$hierKey] = array(
'data' => array( 'data' => array(
@ -190,6 +192,17 @@ if ($resql) {
'classes' => 'hierarchy-edge' '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( $elements['nodes'][] = array(

View file

@ -51,6 +51,12 @@ if ($action === 'save') {
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage"; $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
$sql .= " SET graph_x = ".$x.", graph_y = ".$y; $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; $sql .= " WHERE rowid = ".$anlageId;
if ($db->query($sql)) { if ($db->query($sql)) {
$saved++; $saved++;
@ -73,7 +79,7 @@ if ($action === 'save') {
} }
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage"; $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; $sql .= " WHERE fk_soc = ".(int)$socId;
if ($contactId > 0) { if ($contactId > 0) {
$sql .= " AND fk_contact = ".(int)$contactId; $sql .= " AND fk_contact = ".(int)$contactId;

View file

@ -622,6 +622,12 @@ class modKundenKarte extends DolibarrModules
// v5.2.0: Halbe TE-Breiten (4.5 TE für Neozed etc.) // v5.2.0: Halbe TE-Breiten (4.5 TE für Neozed etc.)
$this->migrate_v520_decimal_te(); $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 // v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge
$this->migrate_v680_bundled_terminals(); $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 * Migration v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge
* Ermöglicht einen Abgang der alle Terminals eines breiten Equipment belegt * Ermöglicht einen Abgang der alle Terminals eines breiten Equipment belegt

View file

@ -400,3 +400,30 @@
width: 160px; 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; }

View file

@ -460,8 +460,8 @@
'text-background-opacity': 0.95, 'text-background-opacity': 0.95,
'text-background-padding': '4px', 'text-background-padding': '4px',
'text-background-shape': 'roundrectangle', 'text-background-shape': 'roundrectangle',
'min-width': '120px', 'min-width': function(node) { return node.data('graph_width') || 120; },
'min-height': '60px' 'min-height': function(node) { return node.data('graph_height') || 60; }
} }
}, },
// Geräte - Karten-Design mit Feldwerten (kompakt) // Geräte - Karten-Design mit Feldwerten (kompakt)
@ -754,9 +754,14 @@
var snappedY = Math.round(pos.y / grid) * grid; var snappedY = Math.round(pos.y / grid) * grid;
node.position({ x: snappedX, y: snappedY }); 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_', ''); 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 // Bei Compound-Nodes auch alle Kinder speichern
var children = node.children(); var children = node.children();
@ -886,13 +891,20 @@
this.editMode = true; this.editMode = true;
this._dirtyNodes = {}; this._dirtyNodes = {};
// Aktuelle Positionen sichern (für Abbrechen) // Aktuelle Positionen und Größen sichern (für Abbrechen)
this._savedPositions = {}; this._savedPositions = {};
this._savedSizes = {};
var self = this; var self = this;
this.cy.nodes().forEach(function(node) { this.cy.nodes().forEach(function(node) {
var pos = node.position(); var pos = node.position();
self._savedPositions[node.data('id')] = { x: pos.x, y: pos.y }; 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 // Nodes verschiebbar machen
this.cy.autoungrabify(false); this.cy.autoungrabify(false);
@ -903,6 +915,9 @@
// Visuelles Feedback: Container-Rahmen // Visuelles Feedback: Container-Rahmen
$('#' + this.containerId).addClass('graph-edit-mode'); $('#' + this.containerId).addClass('graph-edit-mode');
// Resize-Handles für Gebäude
this.showResizeHandles();
}, },
/** /**
@ -912,6 +927,7 @@
if (!this.cy) return; if (!this.cy) return;
this.editMode = false; this.editMode = false;
this._savedPositions = {}; this._savedPositions = {};
this._savedSizes = {};
// Nodes wieder sperren // Nodes wieder sperren
this.cy.autoungrabify(true); this.cy.autoungrabify(true);
@ -922,10 +938,13 @@
// Visuelles Feedback entfernen // Visuelles Feedback entfernen
$('#' + this.containerId).removeClass('graph-edit-mode'); $('#' + 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() { cancelEditMode: function() {
if (!this.cy) return; 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._dirtyNodes = {};
this.exitEditMode(); 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 = $('<div class="kundenkarte-resize-handle resize-' + pos + '" data-node="' + nodeId + '" data-pos="' + pos + '"></div>');
$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 * Geänderte Positionen an Server senden
*/ */

View file

@ -549,3 +549,9 @@ TreeView = Baumansicht
GraphView = Graph-Ansicht GraphView = Graph-Ansicht
GraphLoading = Graph wird geladen... GraphLoading = Graph wird geladen...
SearchPlaceholder = Suchen... SearchPlaceholder = Suchen...
# Ansichtsmodus pro System
ViewModes = Verfuegbare Ansichten
ViewModesBoth = Baum & Graph
ViewModesTreeOnly = Nur Baum
ViewModesGraphOnly = Nur Graph

View file

@ -297,3 +297,9 @@ TreeView = Tree View
GraphView = Graph View GraphView = Graph View
GraphLoading = Loading graph... GraphLoading = Loading graph...
SearchPlaceholder = Search... SearchPlaceholder = Search...
# View modes per system
ViewModes = Available Views
ViewModesBoth = Tree & Graph
ViewModesTreeOnly = Tree Only
ViewModesGraphOnly = Graph Only

View file

@ -47,6 +47,7 @@ function kundenkarte_graph_print_toolbar($params)
$viewMode = $params['viewMode'] ?? 'tree'; $viewMode = $params['viewMode'] ?? 'tree';
$permissiontoadd = !empty($params['permissiontoadd']); $permissiontoadd = !empty($params['permissiontoadd']);
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF']; $pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
$allowedViews = $params['allowedViews'] ?? 'both';
// View-Toggle URL: ID-Parameter je nach Kontext // View-Toggle URL: ID-Parameter je nach Kontext
$idParam = ($contactId > 0) ? $contactId : $socId; $idParam = ($contactId > 0) ? $contactId : $socId;
@ -65,7 +66,9 @@ function kundenkarte_graph_print_toolbar($params)
// Zeile 1: Ansicht-Wechsel + Aktionen // Zeile 1: Ansicht-Wechsel + Aktionen
print '<div class="kundenkarte-graph-toolbar-row">'; print '<div class="kundenkarte-graph-toolbar-row">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>'; if ($allowedViews === 'both') {
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>';
}
if ($permissiontoadd) { 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="'.$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 '<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>';

View file

@ -19,6 +19,9 @@ CREATE TABLE llx_c_kundenkarte_anlage_system
-- Tree display configuration (JSON) -- Tree display configuration (JSON)
tree_display_config text COMMENT 'JSON config for tree display options', 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, position integer DEFAULT 0,
active tinyint DEFAULT 1 NOT NULL active tinyint DEFAULT 1 NOT NULL
) ENGINE=innodb; ) ENGINE=innodb;

View file

@ -366,12 +366,23 @@ if ($action == 'togglepin' && $permissiontoadd) {
$title = $langs->trans('TechnicalInstallations').' - '.$object->name; $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'); $defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
$viewMode = GETPOST('view', 'aZ09'); $viewMode = GETPOST('view', 'aZ09');
if (!in_array($viewMode, array('tree', 'graph'))) { if (!in_array($viewMode, array('tree', 'graph'))) {
$viewMode = $defaultView; $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'); 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 = '<a href="'.DOL_URL_ROOT.'/societe/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>'; $linkback = '<a href="'.DOL_URL_ROOT.'/societe/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom'); 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 '<div class="kundenkarte-admin-shortcut" style="text-align:right;margin:-8px 0 4px 0;">';
print '<a href="'.$adminUrl.'" title="'.$langs->trans('ModuleSetup').'" style="color:var(--colortextlink,#666);font-size:20px;opacity:0.7;"><i class="fa fa-cog"></i></a>';
print '</div>';
}
print '<div class="fichecenter">'; print '<div class="fichecenter">';
// Confirmation dialogs // Confirmation dialogs
@ -474,9 +493,11 @@ if ($isTreeView) {
if ($viewMode !== 'graph') { if ($viewMode !== 'graph') {
// Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper) // Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper)
print '<div class="kundenkarte-tree-controls">'; print '<div class="kundenkarte-tree-controls">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">'; if ($allowedViews === 'both') {
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel; print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
print '</a>'; 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 '<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 '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>'; print '</button>';
@ -505,6 +526,7 @@ if ($isTreeView && $viewMode === 'graph') {
'contactid' => 0, 'contactid' => 0,
'systemid' => $systemId, 'systemid' => $systemId,
'viewMode' => $viewMode, 'viewMode' => $viewMode,
'allowedViews' => $allowedViews,
'permissiontoadd' => $permissiontoadd, 'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'], 'pageUrl' => $_SERVER['PHP_SELF'],
)); ));
@ -1066,18 +1088,8 @@ if (empty($customerSystems)) {
$db->free($resql); $db->free($resql);
} }
// Pre-load all connections for this customer/system // Verbindungen werden nur im Graph angezeigt, nicht im Baum
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)
$connectionsByTarget = array(); $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)) { if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">'; print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">';
@ -1289,7 +1301,7 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) { if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$socid.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
} }
@ -1553,7 +1565,7 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) { if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$socid.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
} }

View file

@ -365,13 +365,24 @@ if ($action == 'togglepin' && $permissiontoadd) {
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs); $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'); 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'); $defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
$viewMode = GETPOST('view', 'aZ09'); $viewMode = GETPOST('view', 'aZ09');
if (!in_array($viewMode, array('tree', 'graph'))) { if (!in_array($viewMode, array('tree', 'graph'))) {
$viewMode = $defaultView; $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()); $jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?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 = '<a href="'.DOL_URL_ROOT.'/contact/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>'; $linkback = '<a href="'.DOL_URL_ROOT.'/contact/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'nom'); 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 '<div class="kundenkarte-admin-shortcut" style="text-align:right;margin:-8px 0 4px 0;">';
print '<a href="'.$adminUrl.'" title="'.$langs->trans('ModuleSetup').'" style="color:var(--colortextlink,#666);font-size:20px;opacity:0.7;"><i class="fa fa-cog"></i></a>';
print '</div>';
}
print '<div class="fichecenter">'; print '<div class="fichecenter">';
// Confirmation dialogs // Confirmation dialogs
@ -472,9 +491,11 @@ if ($isTreeView) {
if ($viewMode !== 'graph') { if ($viewMode !== 'graph') {
// Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper) // Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper)
print '<div class="kundenkarte-tree-controls">'; print '<div class="kundenkarte-tree-controls">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">'; if ($allowedViews === 'both') {
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel; print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
print '</a>'; 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 '<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 '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>'; print '</button>';
@ -503,6 +524,7 @@ if ($isTreeView && $viewMode === 'graph') {
'contactid' => $id, 'contactid' => $id,
'systemid' => $systemId, 'systemid' => $systemId,
'viewMode' => $viewMode, 'viewMode' => $viewMode,
'allowedViews' => $allowedViews,
'permissiontoadd' => $permissiontoadd, 'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'], 'pageUrl' => $_SERVER['PHP_SELF'],
)); ));
@ -1064,18 +1086,8 @@ if (empty($customerSystems)) {
$db->free($resql); $db->free($resql);
} }
// Pre-load all connections for this contact/system // Verbindungen werden nur im Graph angezeigt, nicht im Baum
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)
$connectionsByTarget = array(); $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)) { if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">'; print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
@ -1287,7 +1299,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) { if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$node->fk_soc.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
} }
@ -1583,7 +1595,7 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) { if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$node->fk_soc.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
} }