Version 5.0.0 - Cytoscape.js Graph-Ansicht & Verbindungsformular
Neues Feature: Interaktive Netzwerk-Visualisierung mit Cytoscape.js - Raeume als Compound-Container, Geraete als Nodes - Kabelverbindungen als Edges (auch raumuebergreifend) - Zwei Layout-Modi: Raeumlich (cose-bilkent) / Technisch (dagre) - Zoom/Pan/Fit, Mausrad-Zoom, Node-Positionen speicherbar - Kabeltyp-Legende, Viewport-Persistenz - Admin-Setting KUNDENKARTE_DEFAULT_VIEW (tree/graph) Verbindungsformular verbessert: - Select-Dropdowns zeigen nur Geraete (keine Gebaeude) - Icons via select2, Gebaeude-Pfad als Kontext - Systemuebergreifende Auswahl, Dolibarr-Action-Konvention Bugfixes: - Kontakt-Redirect nach Verbindung-Bearbeitung - contactid in allen Edit-URLs von contact_anlagen.php Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a151270ec
commit
840c0132c3
20 changed files with 15021 additions and 127 deletions
50
ChangeLog.md
50
ChangeLog.md
|
|
@ -1,5 +1,55 @@
|
||||||
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||||
|
|
||||||
|
## 5.0.0 (2026-02)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
- **Cytoscape.js Graph-Ansicht**: Neue interaktive Netzwerk-Visualisierung
|
||||||
|
- Raeume als Compound-Container, Geraete als Nodes darin
|
||||||
|
- Kabelverbindungen als sichtbare Edges (auch raumuebergreifend)
|
||||||
|
- Durchgeschleifte Leitungen als gestrichelte Linien
|
||||||
|
- Zwei Layout-Modi per Knopfdruck:
|
||||||
|
- **Raeumlich** (cose-bilkent): Geraete gruppiert nach Raeumen
|
||||||
|
- **Technisch** (dagre): Hierarchischer Stromfluss top-down
|
||||||
|
- Zoom/Pan/Fit-Controls, Mausrad-Zoom Toggle
|
||||||
|
- Kabeltyp-Legende mit Farben
|
||||||
|
- Node-Positionen speicherbar (Drag&Drop → AJAX-Speicherung)
|
||||||
|
- Viewport-Persistenz (Zoom/Pan bleibt beim Seitenwechsel)
|
||||||
|
- Klick auf Node/Edge oeffnet Detail-/Bearbeitungsseite
|
||||||
|
- Admin-Setting: Ansichtsmodus (Baum/Graph) in Setup waehlbar
|
||||||
|
|
||||||
|
- **Verbindungsformular verbessert**
|
||||||
|
- Select-Dropdowns zeigen nur Geraete (keine Gebaeude/Raeume)
|
||||||
|
- Icons (FontAwesome) in Select-Optionen via select2
|
||||||
|
- Gebaeude-Pfad als Kontext (z.B. "EG > Zahlerschrank")
|
||||||
|
- Systemuebergreifende Geraete-Auswahl (kein Systemfilter)
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
- `js/kundenkarte_cytoscape.js` - Graph-Namespace (~750 Zeilen)
|
||||||
|
- `css/kundenkarte_cytoscape.css` - Graph-Styles mit Dark Mode
|
||||||
|
- `ajax/graph_data.php` - AJAX: Baum+Verbindungen → Cytoscape-Format
|
||||||
|
- `ajax/graph_save_positions.php` - AJAX: Node-Positionen speichern
|
||||||
|
- `js/cytoscape.min.js`, `js/dagre.min.js`, `js/cytoscape-dagre.js`, `js/cytoscape-cose-bilkent.js` - Bibliotheken
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- **Verbindung hinzufuegen**: Formular zeigte "Feld erforderlich"-Fehler beim Oeffnen
|
||||||
|
- Ursache: `action=create` in URL triggerte Handler vor Formular-Anzeige
|
||||||
|
- Fix: Korrekte Dolibarr-Konvention (create=Formular, add=Verarbeitung)
|
||||||
|
- **Leere Dropdowns**: Quelle/Ziel-Auswahl war leer wenn System keine Elemente hatte
|
||||||
|
- Fix: Kein System-Filter mehr (Kabel koennen systemuebergreifend sein)
|
||||||
|
- **Kontakt-Redirect**: Nach Verbindung-Bearbeiten landete man auf Kundenansicht statt Kontaktansicht
|
||||||
|
- Fix: `contactid` wird jetzt in allen Edit-URLs mitgegeben
|
||||||
|
- **Kontakt-Anlagen**: Auf Stand von Kunden-Anlagen gebracht
|
||||||
|
- tree_display_mode, badge_color, Schaltplan-Editor, Drag&Drop Upload
|
||||||
|
|
||||||
|
### Datenbank-Aenderungen
|
||||||
|
- Neue Spalten `graph_x`, `graph_y` in `llx_kundenkarte_anlage` (Node-Positionen)
|
||||||
|
- Neue Spalte `fk_building_node` in `llx_kundenkarte_anlage` (vorbereitet fuer Phase 2)
|
||||||
|
|
||||||
|
### Admin-Settings
|
||||||
|
- `KUNDENKARTE_DEFAULT_VIEW`: `tree` (Standard) oder `graph`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4.0.1 (2026-02)
|
## 4.0.1 (2026-02)
|
||||||
|
|
||||||
### Neue Features
|
### Neue Features
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ if ($action == 'update') {
|
||||||
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_CONTENT', GETPOSTINT('KUNDENKARTE_PDF_FONT_CONTENT'), 'chaine', 0, '', $conf->entity);
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_CONTENT', GETPOSTINT('KUNDENKARTE_PDF_FONT_CONTENT'), 'chaine', 0, '', $conf->entity);
|
||||||
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_FIELDS', GETPOSTINT('KUNDENKARTE_PDF_FONT_FIELDS'), 'chaine', 0, '', $conf->entity);
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_FIELDS', GETPOSTINT('KUNDENKARTE_PDF_FONT_FIELDS'), 'chaine', 0, '', $conf->entity);
|
||||||
|
|
||||||
|
// View mode
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_DEFAULT_VIEW', GETPOST('KUNDENKARTE_DEFAULT_VIEW', 'aZ09'), 'chaine', 0, '', $conf->entity);
|
||||||
|
|
||||||
// Tree display settings
|
// Tree display settings
|
||||||
dolibarr_set_const($db, 'KUNDENKARTE_TREE_INFO_DISPLAY', GETPOST('KUNDENKARTE_TREE_INFO_DISPLAY', 'aZ09'), 'chaine', 0, '', $conf->entity);
|
dolibarr_set_const($db, 'KUNDENKARTE_TREE_INFO_DISPLAY', GETPOST('KUNDENKARTE_TREE_INFO_DISPLAY', 'aZ09'), 'chaine', 0, '', $conf->entity);
|
||||||
dolibarr_set_const($db, 'KUNDENKARTE_TREE_BADGE_COLOR', GETPOST('KUNDENKARTE_TREE_BADGE_COLOR', 'alphanohtml'), 'chaine', 0, '', $conf->entity);
|
dolibarr_set_const($db, 'KUNDENKARTE_TREE_BADGE_COLOR', GETPOST('KUNDENKARTE_TREE_BADGE_COLOR', 'alphanohtml'), 'chaine', 0, '', $conf->entity);
|
||||||
|
|
@ -192,6 +195,18 @@ print $form->selectarray('KUNDENKARTE_DEFAULT_ORDER_TYPE', $orderTypes, getDolGl
|
||||||
print '</td>';
|
print '</td>';
|
||||||
print '</tr>';
|
print '</tr>';
|
||||||
|
|
||||||
|
// Default View Mode for Anlagen
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("DefaultViewMode").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
$viewModes = array(
|
||||||
|
'tree' => $langs->trans("ViewModeTree"),
|
||||||
|
'graph' => $langs->trans("ViewModeGraph"),
|
||||||
|
);
|
||||||
|
print $form->selectarray('KUNDENKARTE_DEFAULT_VIEW', $viewModes, getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'));
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
print '</table>';
|
print '</table>';
|
||||||
|
|
||||||
// Tree Display Settings
|
// Tree Display Settings
|
||||||
|
|
|
||||||
242
ajax/graph_data.php
Normal file
242
ajax/graph_data.php
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX-Endpunkt: Liefert Anlagen als Cytoscape.js Graph
|
||||||
|
* Gebäude-Typen (type_system_code=GLOBAL) = Räume (Compound-Container)
|
||||||
|
* Geräte-Typen = Geräte (Nodes innerhalb der Räume)
|
||||||
|
* Hierarchie über fk_parent (wie im Baum)
|
||||||
|
* Connections = Kabel-Edges zwischen Geräten
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||||
|
|
||||||
|
$res = 0;
|
||||||
|
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||||
|
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
dol_include_once('/kundenkarte/class/anlageconnection.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($socId <= 0) {
|
||||||
|
$response['error'] = 'Missing socid';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
$sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field WHERE active = 1 ORDER BY position";
|
||||||
|
$resFields = $db->query($sqlFields);
|
||||||
|
if ($resFields) {
|
||||||
|
while ($fObj = $db->fetch_object($resFields)) {
|
||||||
|
if ($fObj->field_type === 'header') continue;
|
||||||
|
$fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array(
|
||||||
|
'label' => $fObj->field_label,
|
||||||
|
'display' => $fObj->tree_display_mode ?: 'badge',
|
||||||
|
'color' => $fObj->badge_color ?: '',
|
||||||
|
'type' => $fObj->field_type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$db->free($resFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elemente laden - OHNE GLOBAL-System (das ist nur die separate Gebäudestruktur)
|
||||||
|
// Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL)
|
||||||
|
// Hierarchie kommt aus fk_parent (wie im Baum)
|
||||||
|
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type,";
|
||||||
|
$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y,";
|
||||||
|
$sql .= " t.label as type_label, t.picto as type_picto,";
|
||||||
|
$sql .= " s.code as system_code, s.label as system_label,";
|
||||||
|
$sql .= " ts.code as type_system_code,";
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('image/jpeg','image/png','image/gif','image/webp')) as image_count,";
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type NOT IN ('image/jpeg','image/png','image/gif','image/webp')) as doc_count";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as ts ON t.fk_system = ts.rowid";
|
||||||
|
$sql .= " WHERE a.fk_soc = ".(int)$socId;
|
||||||
|
$sql .= " AND s.code != 'GLOBAL'";
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$sql .= " AND a.fk_contact = ".(int)$contactId;
|
||||||
|
}
|
||||||
|
$sql .= " AND a.status = 1";
|
||||||
|
$sql .= " ORDER BY a.fk_parent, a.rang, a.rowid";
|
||||||
|
|
||||||
|
$elements = array('nodes' => array(), 'edges' => array());
|
||||||
|
$nodeIds = array();
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
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');
|
||||||
|
|
||||||
|
$nodeId = 'n_'.$obj->rowid;
|
||||||
|
$nodeIds[$obj->rowid] = true;
|
||||||
|
|
||||||
|
$nodeData = array(
|
||||||
|
'id' => $nodeId,
|
||||||
|
'label' => $obj->label,
|
||||||
|
'type_label' => $obj->type_label ?: '',
|
||||||
|
'type_picto' => $obj->type_picto ?: '',
|
||||||
|
'system_code' => $obj->system_code ?: '',
|
||||||
|
'system_label' => $obj->system_label ?: '',
|
||||||
|
'fk_parent' => (int) $obj->fk_parent,
|
||||||
|
'fk_anlage_type' => (int) $obj->fk_anlage_type,
|
||||||
|
'is_building' => $isBuilding,
|
||||||
|
'image_count' => (int) $obj->image_count,
|
||||||
|
'doc_count' => (int) $obj->doc_count,
|
||||||
|
'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null,
|
||||||
|
'graph_y' => $obj->graph_y !== null ? (float) $obj->graph_y : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Feldwerte mit Metadaten (Label, Display-Modus, Badge-Farbe)
|
||||||
|
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) {
|
||||||
|
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') {
|
||||||
|
$val = $val ? '1' : '0';
|
||||||
|
}
|
||||||
|
$fields[] = array(
|
||||||
|
'label' => $label,
|
||||||
|
'value' => $val,
|
||||||
|
'display' => $display,
|
||||||
|
'color' => isset($fm['color']) ? $fm['color'] : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$nodeData['fields'] = $fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compound-Parent aus fk_parent (wie im Baum)
|
||||||
|
if ($obj->fk_parent > 0) {
|
||||||
|
$nodeData['parent'] = 'n_'.$obj->fk_parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements['nodes'][] = array(
|
||||||
|
'data' => $nodeData,
|
||||||
|
'classes' => $isBuilding ? 'building-node' : 'device-node'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbindungen laden
|
||||||
|
$connObj = new AnlageConnection($db);
|
||||||
|
$connections = $connObj->fetchBySociete($socId, 0);
|
||||||
|
|
||||||
|
// Verwendete Kabeltypen für die Legende sammeln
|
||||||
|
$usedCableTypes = array();
|
||||||
|
|
||||||
|
foreach ($connections as $conn) {
|
||||||
|
// Nur Edges für tatsächlich geladene Nodes
|
||||||
|
if (!isset($nodeIds[$conn->fk_source]) || !isset($nodeIds[$conn->fk_target])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPassthrough = empty($conn->label) && empty($conn->medium_type_label) && empty($conn->medium_type_text) && empty($conn->medium_spec);
|
||||||
|
|
||||||
|
// Edge-Label zusammenbauen
|
||||||
|
$edgeLabel = '';
|
||||||
|
$mediumType = !empty($conn->medium_type_label) ? $conn->medium_type_label : $conn->medium_type_text;
|
||||||
|
if (!empty($mediumType)) {
|
||||||
|
$edgeLabel = $mediumType;
|
||||||
|
if (!empty($conn->medium_spec)) {
|
||||||
|
$edgeLabel .= ' '.$conn->medium_spec;
|
||||||
|
}
|
||||||
|
if (!empty($conn->medium_length)) {
|
||||||
|
$edgeLabel .= ', '.$conn->medium_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farbe: aus Connection oder aus Medium-Type
|
||||||
|
$color = $conn->medium_color;
|
||||||
|
if (empty($color) && !empty($conn->fk_medium_type)) {
|
||||||
|
// Farbe aus Medium-Type-Tabelle holen
|
||||||
|
$sqlColor = "SELECT color, label_short FROM ".MAIN_DB_PREFIX."kundenkarte_medium_type WHERE rowid = ".(int)$conn->fk_medium_type;
|
||||||
|
$resColor = $db->query($sqlColor);
|
||||||
|
if ($resColor && $mtObj = $db->fetch_object($resColor)) {
|
||||||
|
$color = $mtObj->color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kabeltyp für Legende merken
|
||||||
|
if (!$isPassthrough && !empty($mediumType)) {
|
||||||
|
$typeKey = $mediumType;
|
||||||
|
if (!isset($usedCableTypes[$typeKey])) {
|
||||||
|
$usedCableTypes[$typeKey] = array(
|
||||||
|
'label' => $mediumType,
|
||||||
|
'color' => $color ?: '#5a8a5a',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements['edges'][] = array(
|
||||||
|
'data' => array(
|
||||||
|
'id' => 'conn_'.$conn->id,
|
||||||
|
'source' => 'n_'.$conn->fk_source,
|
||||||
|
'target' => 'n_'.$conn->fk_target,
|
||||||
|
'label' => $edgeLabel,
|
||||||
|
'medium_type' => $mediumType,
|
||||||
|
'medium_spec' => $conn->medium_spec,
|
||||||
|
'medium_length' => $conn->medium_length,
|
||||||
|
'medium_color' => $color,
|
||||||
|
'connection_id' => (int) $conn->id,
|
||||||
|
'is_passthrough' => $isPassthrough,
|
||||||
|
),
|
||||||
|
'classes' => $isPassthrough ? 'passthrough-edge' : 'cable-edge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob gespeicherte Positionen vorhanden sind
|
||||||
|
$hasPositions = false;
|
||||||
|
foreach ($elements['nodes'] as $node) {
|
||||||
|
if ($node['data']['graph_x'] !== null) {
|
||||||
|
$hasPositions = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['elements'] = $elements;
|
||||||
|
$response['cable_types'] = array_values($usedCableTypes);
|
||||||
|
$response['has_positions'] = $hasPositions;
|
||||||
|
$response['meta'] = array(
|
||||||
|
'socid' => $socId,
|
||||||
|
'contactid' => $contactId,
|
||||||
|
'total_nodes' => count($elements['nodes']),
|
||||||
|
'total_connections' => count($connections),
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
93
ajax/graph_save_positions.php
Normal file
93
ajax/graph_save_positions.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX-Endpunkt: Graph-Positionen speichern
|
||||||
|
* Speichert x/y-Koordinaten der Nodes nach Drag&Drop
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||||
|
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
|
||||||
|
|
||||||
|
$res = 0;
|
||||||
|
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||||
|
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Keine Berechtigung';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ');
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
// Positionen als JSON-Array: [{id: 123, x: 45.6, y: 78.9}, ...]
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$input = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!is_array($input) || empty($input['positions'])) {
|
||||||
|
$response['error'] = 'Keine Positionen übergeben';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$saved = 0;
|
||||||
|
$db->begin();
|
||||||
|
|
||||||
|
foreach ($input['positions'] as $pos) {
|
||||||
|
$anlageId = (int) ($pos['id'] ?? 0);
|
||||||
|
$x = (float) ($pos['x'] ?? 0);
|
||||||
|
$y = (float) ($pos['y'] ?? 0);
|
||||||
|
if ($anlageId <= 0) continue;
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " SET graph_x = ".$x.", graph_y = ".$y;
|
||||||
|
$sql .= " WHERE rowid = ".$anlageId;
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
$saved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['saved'] = $saved;
|
||||||
|
|
||||||
|
} elseif ($action === 'reset') {
|
||||||
|
// Alle Positionen für einen Kunden zurücksetzen
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
|
||||||
|
if ($socId <= 0) {
|
||||||
|
$response['error'] = 'Fehlende socid';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " SET graph_x = NULL, graph_y = NULL";
|
||||||
|
$sql .= " WHERE fk_soc = ".(int)$socId;
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$sql .= " AND fk_contact = ".(int)$contactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['reset'] = $db->affected_rows;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Datenbankfehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Unbekannte Aktion';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
|
@ -18,6 +18,7 @@ $langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
$id = GETPOSTINT('id');
|
$id = GETPOSTINT('id');
|
||||||
$socId = GETPOSTINT('socid');
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
$systemId = GETPOSTINT('system_id');
|
$systemId = GETPOSTINT('system_id');
|
||||||
$sourceId = GETPOSTINT('source_id');
|
$sourceId = GETPOSTINT('source_id');
|
||||||
$action = GETPOST('action', 'aZ09');
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
@ -49,6 +50,13 @@ if ($id > 0) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redirect-URL: zurück zur Kontakt- oder Kunden-Anlagenansicht
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$backUrl = dol_buildpath('/kundenkarte/tabs/contact_anlagen.php', 1).'?id='.$contactId.'&system='.$systemId;
|
||||||
|
} else {
|
||||||
|
$backUrl = dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Actions
|
* Actions
|
||||||
*/
|
*/
|
||||||
|
|
@ -71,7 +79,7 @@ if ($action == 'update' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
$result = $connection->update($user);
|
$result = $connection->update($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
|
header('Location: '.$backUrl);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($connection->error, null, 'errors');
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
|
@ -79,7 +87,7 @@ if ($action == 'update' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action == 'create' && $user->hasRight('kundenkarte', 'write')) {
|
if ($action == 'add' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
$connection->fk_source = GETPOSTINT('fk_source');
|
$connection->fk_source = GETPOSTINT('fk_source');
|
||||||
$connection->fk_target = GETPOSTINT('fk_target');
|
$connection->fk_target = GETPOSTINT('fk_target');
|
||||||
$connection->label = GETPOST('label', 'alphanohtml');
|
$connection->label = GETPOST('label', 'alphanohtml');
|
||||||
|
|
@ -98,7 +106,7 @@ if ($action == 'create' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
$result = $connection->create($user);
|
$result = $connection->create($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
|
header('Location: '.$backUrl);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($connection->error, null, 'errors');
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
|
@ -110,7 +118,7 @@ if ($action == 'delete' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
$result = $connection->delete($user);
|
$result = $connection->delete($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
|
header('Location: '.$backUrl);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($connection->error, null, 'errors');
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
|
@ -124,16 +132,56 @@ if ($action == 'delete' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
$title = $id > 0 ? 'Verbindung bearbeiten' : 'Neue Verbindung';
|
$title = $id > 0 ? 'Verbindung bearbeiten' : 'Neue Verbindung';
|
||||||
llxHeader('', $title);
|
llxHeader('', $title);
|
||||||
|
|
||||||
// Load anlagen for dropdowns
|
// Gebäude-Typ-IDs ermitteln (Verbindungen nur zwischen Geräten, nicht Gebäuden)
|
||||||
|
$buildingTypeIds = array();
|
||||||
|
$sqlBt = "SELECT t.rowid FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type t";
|
||||||
|
$sqlBt .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system ts ON t.fk_system = ts.rowid";
|
||||||
|
$sqlBt .= " WHERE ts.code = 'GLOBAL'";
|
||||||
|
$resBt = $db->query($sqlBt);
|
||||||
|
if ($resBt) {
|
||||||
|
while ($btObj = $db->fetch_object($resBt)) {
|
||||||
|
$buildingTypeIds[] = (int) $btObj->rowid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Elemente für Dropdowns laden (OHNE System-Filter, da Kabel systemübergreifend sein können)
|
||||||
$anlagenList = array();
|
$anlagenList = array();
|
||||||
if ($socId > 0) {
|
if ($socId > 0) {
|
||||||
$tree = $anlage->fetchTree($socId, $systemId);
|
if ($contactId > 0) {
|
||||||
// Flatten tree
|
$tree = $anlage->fetchTreeByContact($socId, $contactId, 0);
|
||||||
$flattenTree = function($nodes, $prefix = '') use (&$flattenTree, &$anlagenList) {
|
} else {
|
||||||
|
$tree = $anlage->fetchTree($socId, 0);
|
||||||
|
}
|
||||||
|
// Baum flach machen - nur Geräte, Gebäude als Pfad-Kontext
|
||||||
|
$flattenTree = function($nodes, $path = '') use (&$flattenTree, &$anlagenList, &$buildingTypeIds) {
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$anlagenList[$node->id] = $prefix . $node->label;
|
$isBuilding = in_array((int) $node->fk_anlage_type, $buildingTypeIds);
|
||||||
|
|
||||||
|
if ($isBuilding) {
|
||||||
|
// Gebäude/Raum: nicht wählbar, aber Pfad als Kontext weitergeben
|
||||||
|
$newPath = $path ? $path.' > '.$node->label : $node->label;
|
||||||
if (!empty($node->children)) {
|
if (!empty($node->children)) {
|
||||||
$flattenTree($node->children, $prefix . ' ');
|
$flattenTree($node->children, $newPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Gerät: in Liste aufnehmen mit Gebäude-Pfad als Kontext
|
||||||
|
$typeInfo = !empty($node->type_short) ? $node->type_short : (!empty($node->type_label) ? $node->type_label : '');
|
||||||
|
$label = '';
|
||||||
|
if (!empty($path)) {
|
||||||
|
$label = $path.' > ';
|
||||||
|
}
|
||||||
|
$label .= $node->label;
|
||||||
|
if (!empty($typeInfo)) {
|
||||||
|
$label .= ' ['.$typeInfo.']';
|
||||||
|
}
|
||||||
|
$anlagenList[$node->id] = array(
|
||||||
|
'label' => $label,
|
||||||
|
'picto' => !empty($node->type_picto) ? $node->type_picto : 'fa-cube',
|
||||||
|
);
|
||||||
|
// Rekursion in Geräte-Kinder
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$flattenTree($node->children, $path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -154,8 +202,9 @@ print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
print '<input type="hidden" name="id" value="'.$id.'">';
|
print '<input type="hidden" name="id" value="'.$id.'">';
|
||||||
print '<input type="hidden" name="socid" value="'.$socId.'">';
|
print '<input type="hidden" name="socid" value="'.$socId.'">';
|
||||||
|
print '<input type="hidden" name="contactid" value="'.$contactId.'">';
|
||||||
print '<input type="hidden" name="system_id" value="'.$systemId.'">';
|
print '<input type="hidden" name="system_id" value="'.$systemId.'">';
|
||||||
print '<input type="hidden" name="action" value="'.($id > 0 ? 'update' : 'create').'">';
|
print '<input type="hidden" name="action" value="'.($id > 0 ? 'update' : 'add').'">';
|
||||||
|
|
||||||
print load_fiche_titre($title, '', 'object_kundenkarte@kundenkarte');
|
print load_fiche_titre($title, '', 'object_kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
|
@ -163,21 +212,21 @@ print '<table class="border centpercent">';
|
||||||
|
|
||||||
// Source
|
// Source
|
||||||
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Von (Quelle)').'</td>';
|
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Von (Quelle)').'</td>';
|
||||||
print '<td><select name="fk_source" class="flat minwidth300">';
|
print '<td><select name="fk_source" id="fk_source" class="flat minwidth300">';
|
||||||
print '<option value="">-- Quelle wählen --</option>';
|
print '<option value="">-- Quelle wählen --</option>';
|
||||||
foreach ($anlagenList as $aid => $alabel) {
|
foreach ($anlagenList as $aid => $ainfo) {
|
||||||
$selected = ($connection->fk_source == $aid || $sourceId == $aid) ? ' selected' : '';
|
$selected = ($connection->fk_source == $aid || $sourceId == $aid) ? ' selected' : '';
|
||||||
print '<option value="'.$aid.'"'.$selected.'>'.dol_escape_htmltag($alabel).'</option>';
|
print '<option value="'.$aid.'" data-picto="'.dol_escape_htmltag($ainfo['picto']).'"'.$selected.'>'.dol_escape_htmltag($ainfo['label']).'</option>';
|
||||||
}
|
}
|
||||||
print '</select></td></tr>';
|
print '</select></td></tr>';
|
||||||
|
|
||||||
// Target
|
// Target
|
||||||
print '<tr><td class="fieldrequired">'.$langs->trans('Nach (Ziel)').'</td>';
|
print '<tr><td class="fieldrequired">'.$langs->trans('Nach (Ziel)').'</td>';
|
||||||
print '<td><select name="fk_target" class="flat minwidth300">';
|
print '<td><select name="fk_target" id="fk_target" class="flat minwidth300">';
|
||||||
print '<option value="">-- Ziel wählen --</option>';
|
print '<option value="">-- Ziel wählen --</option>';
|
||||||
foreach ($anlagenList as $aid => $alabel) {
|
foreach ($anlagenList as $aid => $ainfo) {
|
||||||
$selected = ($connection->fk_target == $aid) ? ' selected' : '';
|
$selected = ($connection->fk_target == $aid) ? ' selected' : '';
|
||||||
print '<option value="'.$aid.'"'.$selected.'>'.dol_escape_htmltag($alabel).'</option>';
|
print '<option value="'.$aid.'" data-picto="'.dol_escape_htmltag($ainfo['picto']).'"'.$selected.'>'.dol_escape_htmltag($ainfo['label']).'</option>';
|
||||||
}
|
}
|
||||||
print '</select></td></tr>';
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
|
@ -223,14 +272,35 @@ print '</table>';
|
||||||
|
|
||||||
print '<div class="center" style="margin-top:20px;">';
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
|
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
|
||||||
print ' <a class="button button-cancel" href="'.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId.'">'.$langs->trans('Cancel').'</a>';
|
print ' <a class="button button-cancel" href="'.$backUrl.'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
|
||||||
if ($id > 0 && $user->hasRight('kundenkarte', 'write')) {
|
if ($id > 0 && $user->hasRight('kundenkarte', 'write')) {
|
||||||
print ' <a class="button button-delete" style="margin-left:20px;" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&socid='.$socId.'&system_id='.$systemId.'&action=delete&token='.newToken().'" onclick="return confirm(\'Verbindung wirklich löschen?\');">'.$langs->trans('Delete').'</a>';
|
print ' <a class="button button-delete" style="margin-left:20px;" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&socid='.$socId.'&contactid='.$contactId.'&system_id='.$systemId.'&action=delete&token='.newToken().'" onclick="return confirm(\'Verbindung wirklich löschen?\');">'.$langs->trans('Delete').'</a>';
|
||||||
}
|
}
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
||||||
print '</form>';
|
print '</form>';
|
||||||
|
|
||||||
|
// Select2 mit Icons für Quelle/Ziel-Dropdowns
|
||||||
|
print '<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
function formatAnlageOption(option) {
|
||||||
|
if (!option.id) return option.text;
|
||||||
|
var picto = $(option.element).data("picto");
|
||||||
|
if (picto) {
|
||||||
|
return $("<span><i class=\"fa " + picto + "\" style=\"width:20px;margin-right:8px;text-align:center;color:#666;\"></i>" + $("<span>").text(option.text).html() + "</span>");
|
||||||
|
}
|
||||||
|
return option.text;
|
||||||
|
}
|
||||||
|
$("#fk_source, #fk_target").select2({
|
||||||
|
templateResult: formatAnlageOption,
|
||||||
|
templateSelection: formatAnlageOption,
|
||||||
|
width: "100%",
|
||||||
|
placeholder: "-- Gerät wählen --",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
|
||||||
llxFooter();
|
llxFooter();
|
||||||
$db->close();
|
$db->close();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class Anlage extends CommonObject
|
||||||
public $fk_anlage_type;
|
public $fk_anlage_type;
|
||||||
public $fk_parent;
|
public $fk_parent;
|
||||||
public $fk_system;
|
public $fk_system;
|
||||||
|
public $fk_building_node;
|
||||||
|
|
||||||
public $manufacturer;
|
public $manufacturer;
|
||||||
public $model;
|
public $model;
|
||||||
|
|
@ -199,6 +200,7 @@ class Anlage extends CommonObject
|
||||||
$this->fk_anlage_type = $obj->fk_anlage_type;
|
$this->fk_anlage_type = $obj->fk_anlage_type;
|
||||||
$this->fk_parent = $obj->fk_parent;
|
$this->fk_parent = $obj->fk_parent;
|
||||||
$this->fk_system = $obj->fk_system;
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->fk_building_node = isset($obj->fk_building_node) ? (int) $obj->fk_building_node : 0;
|
||||||
|
|
||||||
$this->manufacturer = $obj->manufacturer;
|
$this->manufacturer = $obj->manufacturer;
|
||||||
$this->model = $obj->model;
|
$this->model = $obj->model;
|
||||||
|
|
|
||||||
|
|
@ -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'
|
$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'
|
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||||
$this->version = '4.0.3';
|
$this->version = '5.0.0';
|
||||||
// Url to the file with your last numberversion of this module
|
// Url to the file with your last numberversion of this module
|
||||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||||
|
|
||||||
|
|
@ -609,6 +609,12 @@ class modKundenKarte extends DolibarrModules
|
||||||
|
|
||||||
// v3.7.0: Add badge color for fields
|
// v3.7.0: Add badge color for fields
|
||||||
$this->migrate_v370_badge_color();
|
$this->migrate_v370_badge_color();
|
||||||
|
|
||||||
|
// v4.1.0: Add fk_building_node for room assignment
|
||||||
|
$this->migrate_v410_building_node();
|
||||||
|
|
||||||
|
// v4.1.0: Graph-Positionen speichern
|
||||||
|
$this->migrate_v410_graph_positions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -703,6 +709,57 @@ class modKundenKarte extends DolibarrModules
|
||||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN badge_color varchar(7) AFTER tree_display_mode");
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN badge_color varchar(7) AFTER tree_display_mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration v4.1.0: Spalte fk_building_node für Raumzuordnung
|
||||||
|
* Trennt Gebäude-/Raumzuordnung von technischem Parent (fk_parent)
|
||||||
|
*/
|
||||||
|
private function migrate_v410_building_node()
|
||||||
|
{
|
||||||
|
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
|
||||||
|
// Prüfen ob Tabelle existiert
|
||||||
|
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||||
|
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob Spalte bereits existiert
|
||||||
|
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_building_node'");
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spalte hinzufügen
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_building_node integer DEFAULT 0 AFTER fk_system");
|
||||||
|
|
||||||
|
// Index für Performance
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD INDEX idx_anlage_building_node (fk_building_node)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration v4.1.0: Graph-Positionen (x/y) in Anlage-Tabelle
|
||||||
|
*/
|
||||||
|
private function migrate_v410_graph_positions()
|
||||||
|
{
|
||||||
|
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
|
||||||
|
// Prüfen ob Tabelle existiert
|
||||||
|
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||||
|
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob Spalte bereits existiert
|
||||||
|
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'graph_x'");
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spalten hinzufügen
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_x double DEFAULT NULL AFTER fk_building_node");
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_y double DEFAULT NULL AFTER graph_x");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called when module is disabled.
|
* Function called when module is disabled.
|
||||||
* Remove from database constants, boxes and permissions from Dolibarr database.
|
* Remove from database constants, boxes and permissions from Dolibarr database.
|
||||||
|
|
|
||||||
229
css/kundenkarte_cytoscape.css
Normal file
229
css/kundenkarte_cytoscape.css
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/**
|
||||||
|
* KundenKarte Graph-Ansicht Styles
|
||||||
|
* Nutzt Dolibarr CSS-Variablen für Theme-Kompatibilität
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Graph Container */
|
||||||
|
.kundenkarte-graph-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kundenkarte-graph-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 80vh;
|
||||||
|
border: 1px solid var(--inputbordercolor, #3a3a3a);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--colorbackbody, #1d1e20);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#kundenkarte-graph-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar: Aktionen links, Zoom rechts */
|
||||||
|
.kundenkarte-graph-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-actions .button {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-zoom-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-graph-wheel-zoom.active {
|
||||||
|
background: var(--butactionbg, #4390dc) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--butactionbg, #4390dc) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legende */
|
||||||
|
.kundenkarte-graph-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--colorbacktitle1, rgba(30, 30, 50, 0.8));
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--colortext, #aaa);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line {
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line.cable {
|
||||||
|
background: #5a8a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line.passthrough {
|
||||||
|
background: none;
|
||||||
|
border-top: 2px dashed #505860;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box {
|
||||||
|
width: 16px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box.building {
|
||||||
|
background: #2a2b2d;
|
||||||
|
border: 1px dashed #4390dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box.device {
|
||||||
|
background: #2d4a3a;
|
||||||
|
border: 2px solid #5a9a6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Overlay */
|
||||||
|
.kundenkarte-graph-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: var(--colortext, #aaa);
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-loading i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Tooltip */
|
||||||
|
.kundenkarte-graph-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--colorbacktabcard1, #1e2a3a);
|
||||||
|
border: 1px solid var(--inputbordercolor, #3a6a8e);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--colortext, #ccc);
|
||||||
|
max-width: 300px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--colortextlink, #7ab0d4);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-type {
|
||||||
|
color: var(--colortext, #888);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field-label {
|
||||||
|
color: var(--colortext, #888);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field-value {
|
||||||
|
color: var(--colortext, #ddd);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leer-Zustand */
|
||||||
|
.kundenkarte-graph-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
color: var(--colortext, #666);
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Anpassungen */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.kundenkarte-graph-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-actions {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-actions .button {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 8px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-zoom-controls {
|
||||||
|
order: 1;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-zoom-controls .button {
|
||||||
|
padding: 10px 8px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
3214
js/cose-base.js
Normal file
3214
js/cose-base.js
Normal file
File diff suppressed because it is too large
Load diff
458
js/cytoscape-cose-bilkent.js
Normal file
458
js/cytoscape-cose-bilkent.js
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
(function webpackUniversalModuleDefinition(root, factory) {
|
||||||
|
if(typeof exports === 'object' && typeof module === 'object')
|
||||||
|
module.exports = factory(require("cose-base"));
|
||||||
|
else if(typeof define === 'function' && define.amd)
|
||||||
|
define(["cose-base"], factory);
|
||||||
|
else if(typeof exports === 'object')
|
||||||
|
exports["cytoscapeCoseBilkent"] = factory(require("cose-base"));
|
||||||
|
else
|
||||||
|
root["cytoscapeCoseBilkent"] = factory(root["coseBase"]);
|
||||||
|
})(this, function(__WEBPACK_EXTERNAL_MODULE_0__) {
|
||||||
|
return /******/ (function(modules) { // webpackBootstrap
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var installedModules = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ if(installedModules[moduleId]) {
|
||||||
|
/******/ return installedModules[moduleId].exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = installedModules[moduleId] = {
|
||||||
|
/******/ i: moduleId,
|
||||||
|
/******/ l: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.l = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = modules;
|
||||||
|
/******/
|
||||||
|
/******/ // expose the module cache
|
||||||
|
/******/ __webpack_require__.c = installedModules;
|
||||||
|
/******/
|
||||||
|
/******/ // identity function for calling harmony imports with the correct context
|
||||||
|
/******/ __webpack_require__.i = function(value) { return value; };
|
||||||
|
/******/
|
||||||
|
/******/ // define getter function for harmony exports
|
||||||
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||||
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||||
|
/******/ Object.defineProperty(exports, name, {
|
||||||
|
/******/ configurable: false,
|
||||||
|
/******/ enumerable: true,
|
||||||
|
/******/ get: getter
|
||||||
|
/******/ });
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = function(module) {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ function getDefault() { return module['default']; } :
|
||||||
|
/******/ function getModuleExports() { return module; };
|
||||||
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Object.prototype.hasOwnProperty.call
|
||||||
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||||
|
/******/
|
||||||
|
/******/ // __webpack_public_path__
|
||||||
|
/******/ __webpack_require__.p = "";
|
||||||
|
/******/
|
||||||
|
/******/ // Load entry module and return exports
|
||||||
|
/******/ return __webpack_require__(__webpack_require__.s = 1);
|
||||||
|
/******/ })
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ ([
|
||||||
|
/* 0 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
module.exports = __WEBPACK_EXTERNAL_MODULE_0__;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 1 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
var LayoutConstants = __webpack_require__(0).layoutBase.LayoutConstants;
|
||||||
|
var FDLayoutConstants = __webpack_require__(0).layoutBase.FDLayoutConstants;
|
||||||
|
var CoSEConstants = __webpack_require__(0).CoSEConstants;
|
||||||
|
var CoSELayout = __webpack_require__(0).CoSELayout;
|
||||||
|
var CoSENode = __webpack_require__(0).CoSENode;
|
||||||
|
var PointD = __webpack_require__(0).layoutBase.PointD;
|
||||||
|
var DimensionD = __webpack_require__(0).layoutBase.DimensionD;
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
// Called on `layoutready`
|
||||||
|
ready: function ready() {},
|
||||||
|
// Called on `layoutstop`
|
||||||
|
stop: function stop() {},
|
||||||
|
// 'draft', 'default' or 'proof"
|
||||||
|
// - 'draft' fast cooling rate
|
||||||
|
// - 'default' moderate cooling rate
|
||||||
|
// - "proof" slow cooling rate
|
||||||
|
quality: 'default',
|
||||||
|
// include labels in node dimensions
|
||||||
|
nodeDimensionsIncludeLabels: false,
|
||||||
|
// number of ticks per frame; higher is faster but more jerky
|
||||||
|
refresh: 30,
|
||||||
|
// Whether to fit the network view after when done
|
||||||
|
fit: true,
|
||||||
|
// Padding on fit
|
||||||
|
padding: 10,
|
||||||
|
// Whether to enable incremental mode
|
||||||
|
randomize: true,
|
||||||
|
// Node repulsion (non overlapping) multiplier
|
||||||
|
nodeRepulsion: 4500,
|
||||||
|
// Ideal edge (non nested) length
|
||||||
|
idealEdgeLength: 50,
|
||||||
|
// Divisor to compute edge forces
|
||||||
|
edgeElasticity: 0.45,
|
||||||
|
// Nesting factor (multiplier) to compute ideal edge length for nested edges
|
||||||
|
nestingFactor: 0.1,
|
||||||
|
// Gravity force (constant)
|
||||||
|
gravity: 0.25,
|
||||||
|
// Maximum number of iterations to perform
|
||||||
|
numIter: 2500,
|
||||||
|
// For enabling tiling
|
||||||
|
tile: true,
|
||||||
|
// Type of layout animation. The option set is {'during', 'end', false}
|
||||||
|
animate: 'end',
|
||||||
|
// Duration for animate:end
|
||||||
|
animationDuration: 500,
|
||||||
|
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
|
||||||
|
tilingPaddingVertical: 10,
|
||||||
|
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
|
||||||
|
tilingPaddingHorizontal: 10,
|
||||||
|
// Gravity range (constant) for compounds
|
||||||
|
gravityRangeCompound: 1.5,
|
||||||
|
// Gravity force (constant) for compounds
|
||||||
|
gravityCompound: 1.0,
|
||||||
|
// Gravity range (constant)
|
||||||
|
gravityRange: 3.8,
|
||||||
|
// Initial cooling factor for incremental layout
|
||||||
|
initialEnergyOnIncremental: 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
function extend(defaults, options) {
|
||||||
|
var obj = {};
|
||||||
|
|
||||||
|
for (var i in defaults) {
|
||||||
|
obj[i] = defaults[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i in options) {
|
||||||
|
obj[i] = options[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _CoSELayout(_options) {
|
||||||
|
this.options = extend(defaults, _options);
|
||||||
|
getUserOptions(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
var getUserOptions = function getUserOptions(options) {
|
||||||
|
if (options.nodeRepulsion != null) CoSEConstants.DEFAULT_REPULSION_STRENGTH = FDLayoutConstants.DEFAULT_REPULSION_STRENGTH = options.nodeRepulsion;
|
||||||
|
if (options.idealEdgeLength != null) CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength;
|
||||||
|
if (options.edgeElasticity != null) CoSEConstants.DEFAULT_SPRING_STRENGTH = FDLayoutConstants.DEFAULT_SPRING_STRENGTH = options.edgeElasticity;
|
||||||
|
if (options.nestingFactor != null) CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor;
|
||||||
|
if (options.gravity != null) CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity;
|
||||||
|
if (options.numIter != null) CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter;
|
||||||
|
if (options.gravityRange != null) CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange;
|
||||||
|
if (options.gravityCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound;
|
||||||
|
if (options.gravityRangeCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound;
|
||||||
|
if (options.initialEnergyOnIncremental != null) CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental;
|
||||||
|
|
||||||
|
if (options.quality == 'draft') LayoutConstants.QUALITY = 0;else if (options.quality == 'proof') LayoutConstants.QUALITY = 2;else LayoutConstants.QUALITY = 1;
|
||||||
|
|
||||||
|
CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels;
|
||||||
|
CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = !options.randomize;
|
||||||
|
CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate;
|
||||||
|
CoSEConstants.TILE = options.tile;
|
||||||
|
CoSEConstants.TILING_PADDING_VERTICAL = typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical;
|
||||||
|
CoSEConstants.TILING_PADDING_HORIZONTAL = typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal;
|
||||||
|
};
|
||||||
|
|
||||||
|
_CoSELayout.prototype.run = function () {
|
||||||
|
var ready;
|
||||||
|
var frameId;
|
||||||
|
var options = this.options;
|
||||||
|
var idToLNode = this.idToLNode = {};
|
||||||
|
var layout = this.layout = new CoSELayout();
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.stopped = false;
|
||||||
|
|
||||||
|
this.cy = this.options.cy;
|
||||||
|
|
||||||
|
this.cy.trigger({ type: 'layoutstart', layout: this });
|
||||||
|
|
||||||
|
var gm = layout.newGraphManager();
|
||||||
|
this.gm = gm;
|
||||||
|
|
||||||
|
var nodes = this.options.eles.nodes();
|
||||||
|
var edges = this.options.eles.edges();
|
||||||
|
|
||||||
|
this.root = gm.addRoot();
|
||||||
|
this.processChildrenList(this.root, this.getTopMostNodes(nodes), layout);
|
||||||
|
|
||||||
|
for (var i = 0; i < edges.length; i++) {
|
||||||
|
var edge = edges[i];
|
||||||
|
var sourceNode = this.idToLNode[edge.data("source")];
|
||||||
|
var targetNode = this.idToLNode[edge.data("target")];
|
||||||
|
if (sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0) {
|
||||||
|
var e1 = gm.add(layout.newEdge(), sourceNode, targetNode);
|
||||||
|
e1.id = edge.id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var getPositions = function getPositions(ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
var theId = ele.data('id');
|
||||||
|
var lNode = self.idToLNode[theId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: lNode.getRect().getCenterX(),
|
||||||
|
y: lNode.getRect().getCenterY()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reposition nodes in iterations animatedly
|
||||||
|
*/
|
||||||
|
var iterateAnimated = function iterateAnimated() {
|
||||||
|
// Thigs to perform after nodes are repositioned on screen
|
||||||
|
var afterReposition = function afterReposition() {
|
||||||
|
if (options.fit) {
|
||||||
|
options.cy.fit(options.eles, options.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = true;
|
||||||
|
self.cy.one('layoutready', options.ready);
|
||||||
|
self.cy.trigger({ type: 'layoutready', layout: self });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ticksPerFrame = self.options.refresh;
|
||||||
|
var isDone;
|
||||||
|
|
||||||
|
for (var i = 0; i < ticksPerFrame && !isDone; i++) {
|
||||||
|
isDone = self.stopped || self.layout.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If layout is done
|
||||||
|
if (isDone) {
|
||||||
|
// If the layout is not a sublayout and it is successful perform post layout.
|
||||||
|
if (layout.checkLayoutSuccess() && !layout.isSubLayout) {
|
||||||
|
layout.doPostLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If layout has a tilingPostLayout function property call it.
|
||||||
|
if (layout.tilingPostLayout) {
|
||||||
|
layout.tilingPostLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.isLayoutFinished = true;
|
||||||
|
|
||||||
|
self.options.eles.nodes().positions(getPositions);
|
||||||
|
|
||||||
|
afterReposition();
|
||||||
|
|
||||||
|
// trigger layoutstop when the layout stops (e.g. finishes)
|
||||||
|
self.cy.one('layoutstop', self.options.stop);
|
||||||
|
self.cy.trigger({ type: 'layoutstop', layout: self });
|
||||||
|
|
||||||
|
if (frameId) {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ready = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationData = self.layout.getPositionsData(); // Get positions of layout nodes note that all nodes may not be layout nodes because of tiling
|
||||||
|
|
||||||
|
// Position nodes, for the nodes whose id does not included in data (because they are removed from their parents and included in dummy compounds)
|
||||||
|
// use position of their ancestors or dummy ancestors
|
||||||
|
options.eles.nodes().positions(function (ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
// If ele is a compound node, then its position will be defined by its children
|
||||||
|
if (!ele.isParent()) {
|
||||||
|
var theId = ele.id();
|
||||||
|
var pNode = animationData[theId];
|
||||||
|
var temp = ele;
|
||||||
|
// If pNode is undefined search until finding position data of its first ancestor (It may be dummy as well)
|
||||||
|
while (pNode == null) {
|
||||||
|
pNode = animationData[temp.data('parent')] || animationData['DummyCompound_' + temp.data('parent')];
|
||||||
|
animationData[theId] = pNode;
|
||||||
|
temp = temp.parent()[0];
|
||||||
|
if (temp == undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pNode != null) {
|
||||||
|
return {
|
||||||
|
x: pNode.x,
|
||||||
|
y: pNode.y
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: ele.position('x'),
|
||||||
|
y: ele.position('y')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterReposition();
|
||||||
|
|
||||||
|
frameId = requestAnimationFrame(iterateAnimated);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Listen 'layoutstarted' event and start animated iteration if animate option is 'during'
|
||||||
|
*/
|
||||||
|
layout.addListener('layoutstarted', function () {
|
||||||
|
if (self.options.animate === 'during') {
|
||||||
|
frameId = requestAnimationFrame(iterateAnimated);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.runLayout(); // Run cose layout
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If animate option is not 'during' ('end' or false) perform these here (If it is 'during' similar things are already performed)
|
||||||
|
*/
|
||||||
|
if (this.options.animate !== "during") {
|
||||||
|
self.options.eles.nodes().not(":parent").layoutPositions(self, self.options, getPositions); // Use layout positions to reposition the nodes it considers the options parameter
|
||||||
|
ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
//Get the top most ones of a list of nodes
|
||||||
|
_CoSELayout.prototype.getTopMostNodes = function (nodes) {
|
||||||
|
var nodesMap = {};
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
nodesMap[nodes[i].id()] = true;
|
||||||
|
}
|
||||||
|
var roots = nodes.filter(function (ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
var parent = ele.parent()[0];
|
||||||
|
while (parent != null) {
|
||||||
|
if (nodesMap[parent.id()]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parent = parent.parent()[0];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return roots;
|
||||||
|
};
|
||||||
|
|
||||||
|
_CoSELayout.prototype.processChildrenList = function (parent, children, layout) {
|
||||||
|
var size = children.length;
|
||||||
|
for (var i = 0; i < size; i++) {
|
||||||
|
var theChild = children[i];
|
||||||
|
var children_of_children = theChild.children();
|
||||||
|
var theNode;
|
||||||
|
|
||||||
|
var dimensions = theChild.layoutDimensions({
|
||||||
|
nodeDimensionsIncludeLabels: this.options.nodeDimensionsIncludeLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theChild.outerWidth() != null && theChild.outerHeight() != null) {
|
||||||
|
theNode = parent.add(new CoSENode(layout.graphManager, new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2), new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
|
||||||
|
} else {
|
||||||
|
theNode = parent.add(new CoSENode(this.graphManager));
|
||||||
|
}
|
||||||
|
// Attach id to the layout node
|
||||||
|
theNode.id = theChild.data("id");
|
||||||
|
// Attach the paddings of cy node to layout node
|
||||||
|
theNode.paddingLeft = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingTop = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingRight = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingBottom = parseInt(theChild.css('padding'));
|
||||||
|
|
||||||
|
//Attach the label properties to compound if labels will be included in node dimensions
|
||||||
|
if (this.options.nodeDimensionsIncludeLabels) {
|
||||||
|
if (theChild.isParent()) {
|
||||||
|
var labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false }).w;
|
||||||
|
var labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false }).h;
|
||||||
|
var labelPos = theChild.css("text-halign");
|
||||||
|
theNode.labelWidth = labelWidth;
|
||||||
|
theNode.labelHeight = labelHeight;
|
||||||
|
theNode.labelPos = labelPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the layout node
|
||||||
|
this.idToLNode[theChild.data("id")] = theNode;
|
||||||
|
|
||||||
|
if (isNaN(theNode.rect.x)) {
|
||||||
|
theNode.rect.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(theNode.rect.y)) {
|
||||||
|
theNode.rect.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children_of_children != null && children_of_children.length > 0) {
|
||||||
|
var theNewGraph;
|
||||||
|
theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode);
|
||||||
|
this.processChildrenList(theNewGraph, children_of_children, layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief : called on continuous layouts to stop them before they finish
|
||||||
|
*/
|
||||||
|
_CoSELayout.prototype.stop = function () {
|
||||||
|
this.stopped = true;
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
var register = function register(cytoscape) {
|
||||||
|
// var Layout = getLayout( cytoscape );
|
||||||
|
|
||||||
|
cytoscape('layout', 'cose-bilkent', _CoSELayout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// auto reg for globals
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
register(cytoscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = register;
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
/******/ ]);
|
||||||
|
});
|
||||||
397
js/cytoscape-dagre.js
Normal file
397
js/cytoscape-dagre.js
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
(function webpackUniversalModuleDefinition(root, factory) {
|
||||||
|
if(typeof exports === 'object' && typeof module === 'object')
|
||||||
|
module.exports = factory(require("dagre"));
|
||||||
|
else if(typeof define === 'function' && define.amd)
|
||||||
|
define(["dagre"], factory);
|
||||||
|
else if(typeof exports === 'object')
|
||||||
|
exports["cytoscapeDagre"] = factory(require("dagre"));
|
||||||
|
else
|
||||||
|
root["cytoscapeDagre"] = factory(root["dagre"]);
|
||||||
|
})(this, function(__WEBPACK_EXTERNAL_MODULE__4__) {
|
||||||
|
return /******/ (function(modules) { // webpackBootstrap
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var installedModules = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ if(installedModules[moduleId]) {
|
||||||
|
/******/ return installedModules[moduleId].exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = installedModules[moduleId] = {
|
||||||
|
/******/ i: moduleId,
|
||||||
|
/******/ l: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.l = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = modules;
|
||||||
|
/******/
|
||||||
|
/******/ // expose the module cache
|
||||||
|
/******/ __webpack_require__.c = installedModules;
|
||||||
|
/******/
|
||||||
|
/******/ // define getter function for harmony exports
|
||||||
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||||
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||||
|
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // define __esModule on exports
|
||||||
|
/******/ __webpack_require__.r = function(exports) {
|
||||||
|
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||||
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||||
|
/******/ }
|
||||||
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // create a fake namespace object
|
||||||
|
/******/ // mode & 1: value is a module id, require it
|
||||||
|
/******/ // mode & 2: merge all properties of value into the ns
|
||||||
|
/******/ // mode & 4: return value when already ns object
|
||||||
|
/******/ // mode & 8|1: behave like require
|
||||||
|
/******/ __webpack_require__.t = function(value, mode) {
|
||||||
|
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||||
|
/******/ if(mode & 8) return value;
|
||||||
|
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||||
|
/******/ var ns = Object.create(null);
|
||||||
|
/******/ __webpack_require__.r(ns);
|
||||||
|
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||||
|
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||||
|
/******/ return ns;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = function(module) {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ function getDefault() { return module['default']; } :
|
||||||
|
/******/ function getModuleExports() { return module; };
|
||||||
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Object.prototype.hasOwnProperty.call
|
||||||
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||||
|
/******/
|
||||||
|
/******/ // __webpack_public_path__
|
||||||
|
/******/ __webpack_require__.p = "";
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // Load entry module and return exports
|
||||||
|
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
||||||
|
/******/ })
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ ([
|
||||||
|
/* 0 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
var impl = __webpack_require__(1); // registers the extension on a cytoscape lib ref
|
||||||
|
|
||||||
|
|
||||||
|
var register = function register(cytoscape) {
|
||||||
|
if (!cytoscape) {
|
||||||
|
return;
|
||||||
|
} // can't register if cytoscape unspecified
|
||||||
|
|
||||||
|
|
||||||
|
cytoscape('layout', 'dagre', impl); // register with cytoscape.js
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
// expose to global cytoscape (i.e. window.cytoscape)
|
||||||
|
register(cytoscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = register;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 1 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
|
||||||
|
|
||||||
|
var isFunction = function isFunction(o) {
|
||||||
|
return typeof o === 'function';
|
||||||
|
};
|
||||||
|
|
||||||
|
var defaults = __webpack_require__(2);
|
||||||
|
|
||||||
|
var assign = __webpack_require__(3);
|
||||||
|
|
||||||
|
var dagre = __webpack_require__(4); // constructor
|
||||||
|
// options : object containing layout options
|
||||||
|
|
||||||
|
|
||||||
|
function DagreLayout(options) {
|
||||||
|
this.options = assign({}, defaults, options);
|
||||||
|
} // runs the layout
|
||||||
|
|
||||||
|
|
||||||
|
DagreLayout.prototype.run = function () {
|
||||||
|
var options = this.options;
|
||||||
|
var layout = this;
|
||||||
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
||||||
|
|
||||||
|
var eles = options.eles;
|
||||||
|
|
||||||
|
var getVal = function getVal(ele, val) {
|
||||||
|
return isFunction(val) ? val.apply(ele, [ele]) : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
var bb = options.boundingBox || {
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
w: cy.width(),
|
||||||
|
h: cy.height()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bb.x2 === undefined) {
|
||||||
|
bb.x2 = bb.x1 + bb.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.w === undefined) {
|
||||||
|
bb.w = bb.x2 - bb.x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.y2 === undefined) {
|
||||||
|
bb.y2 = bb.y1 + bb.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.h === undefined) {
|
||||||
|
bb.h = bb.y2 - bb.y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var g = new dagre.graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
});
|
||||||
|
var gObj = {};
|
||||||
|
|
||||||
|
var setGObj = function setGObj(name, val) {
|
||||||
|
if (val != null) {
|
||||||
|
gObj[name] = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setGObj('nodesep', options.nodeSep);
|
||||||
|
setGObj('edgesep', options.edgeSep);
|
||||||
|
setGObj('ranksep', options.rankSep);
|
||||||
|
setGObj('rankdir', options.rankDir);
|
||||||
|
setGObj('align', options.align);
|
||||||
|
setGObj('ranker', options.ranker);
|
||||||
|
setGObj('acyclicer', options.acyclicer);
|
||||||
|
g.setGraph(gObj);
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
g.setDefaultNodeLabel(function () {
|
||||||
|
return {};
|
||||||
|
}); // add nodes to dagre
|
||||||
|
|
||||||
|
var nodes = eles.nodes();
|
||||||
|
|
||||||
|
if (isFunction(options.sort)) {
|
||||||
|
nodes = nodes.sort(options.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
var node = nodes[i];
|
||||||
|
var nbb = node.layoutDimensions(options);
|
||||||
|
g.setNode(node.id(), {
|
||||||
|
width: nbb.w,
|
||||||
|
height: nbb.h,
|
||||||
|
name: node.id()
|
||||||
|
}); // console.log( g.node(node.id()) );
|
||||||
|
} // set compound parents
|
||||||
|
|
||||||
|
|
||||||
|
for (var _i = 0; _i < nodes.length; _i++) {
|
||||||
|
var _node = nodes[_i];
|
||||||
|
|
||||||
|
if (_node.isChild()) {
|
||||||
|
g.setParent(_node.id(), _node.parent().id());
|
||||||
|
}
|
||||||
|
} // add edges to dagre
|
||||||
|
|
||||||
|
|
||||||
|
var edges = eles.edges().stdFilter(function (edge) {
|
||||||
|
return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFunction(options.sort)) {
|
||||||
|
edges = edges.sort(options.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var _i2 = 0; _i2 < edges.length; _i2++) {
|
||||||
|
var edge = edges[_i2];
|
||||||
|
g.setEdge(edge.source().id(), edge.target().id(), {
|
||||||
|
minlen: getVal(edge, options.minLen),
|
||||||
|
weight: getVal(edge, options.edgeWeight),
|
||||||
|
name: edge.id()
|
||||||
|
}, edge.id()); // console.log( g.edge(edge.source().id(), edge.target().id(), edge.id()) );
|
||||||
|
}
|
||||||
|
|
||||||
|
dagre.layout(g);
|
||||||
|
var gNodeIds = g.nodes();
|
||||||
|
|
||||||
|
for (var _i3 = 0; _i3 < gNodeIds.length; _i3++) {
|
||||||
|
var id = gNodeIds[_i3];
|
||||||
|
var n = g.node(id);
|
||||||
|
cy.getElementById(id).scratch().dagre = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dagreBB;
|
||||||
|
|
||||||
|
if (options.boundingBox) {
|
||||||
|
dagreBB = {
|
||||||
|
x1: Infinity,
|
||||||
|
x2: -Infinity,
|
||||||
|
y1: Infinity,
|
||||||
|
y2: -Infinity
|
||||||
|
};
|
||||||
|
nodes.forEach(function (node) {
|
||||||
|
var dModel = node.scratch().dagre;
|
||||||
|
dagreBB.x1 = Math.min(dagreBB.x1, dModel.x);
|
||||||
|
dagreBB.x2 = Math.max(dagreBB.x2, dModel.x);
|
||||||
|
dagreBB.y1 = Math.min(dagreBB.y1, dModel.y);
|
||||||
|
dagreBB.y2 = Math.max(dagreBB.y2, dModel.y);
|
||||||
|
});
|
||||||
|
dagreBB.w = dagreBB.x2 - dagreBB.x1;
|
||||||
|
dagreBB.h = dagreBB.y2 - dagreBB.y1;
|
||||||
|
} else {
|
||||||
|
dagreBB = bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
var constrainPos = function constrainPos(p) {
|
||||||
|
if (options.boundingBox) {
|
||||||
|
var xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
|
||||||
|
var yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
|
||||||
|
return {
|
||||||
|
x: bb.x1 + xPct * bb.w,
|
||||||
|
y: bb.y1 + yPct * bb.h
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.layoutPositions(layout, options, function (ele) {
|
||||||
|
ele = _typeof(ele) === "object" ? ele : this;
|
||||||
|
var dModel = ele.scratch().dagre;
|
||||||
|
return constrainPos({
|
||||||
|
x: dModel.x,
|
||||||
|
y: dModel.y
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = DagreLayout;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 2 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
// dagre algo options, uses default value on undefined
|
||||||
|
nodeSep: undefined,
|
||||||
|
// the separation between adjacent nodes in the same rank
|
||||||
|
edgeSep: undefined,
|
||||||
|
// the separation between adjacent edges in the same rank
|
||||||
|
rankSep: undefined,
|
||||||
|
// the separation between adjacent nodes in the same rank
|
||||||
|
rankDir: undefined,
|
||||||
|
// 'TB' for top to bottom flow, 'LR' for left to right,
|
||||||
|
align: undefined,
|
||||||
|
// alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR', where U = up, D = down, L = left, and R = right
|
||||||
|
acyclicer: undefined,
|
||||||
|
// If set to 'greedy', uses a greedy heuristic for finding a feedback arc set for a graph.
|
||||||
|
// A feedback arc set is a set of edges that can be removed to make a graph acyclic.
|
||||||
|
ranker: undefined,
|
||||||
|
// Type of algorithm to assigns a rank to each node in the input graph.
|
||||||
|
// Possible values: network-simplex, tight-tree or longest-path
|
||||||
|
minLen: function minLen(edge) {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
// number of ranks to keep between the source and target of the edge
|
||||||
|
edgeWeight: function edgeWeight(edge) {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
// higher weight edges are generally made shorter and straighter than lower weight edges
|
||||||
|
// general layout options
|
||||||
|
fit: true,
|
||||||
|
// whether to fit to viewport
|
||||||
|
padding: 30,
|
||||||
|
// fit padding
|
||||||
|
spacingFactor: undefined,
|
||||||
|
// Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
|
||||||
|
nodeDimensionsIncludeLabels: false,
|
||||||
|
// whether labels should be included in determining the space used by a node
|
||||||
|
animate: false,
|
||||||
|
// whether to transition the node positions
|
||||||
|
animateFilter: function animateFilter(node, i) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
|
||||||
|
animationDuration: 500,
|
||||||
|
// duration of animation in ms if enabled
|
||||||
|
animationEasing: undefined,
|
||||||
|
// easing of animation if enabled
|
||||||
|
boundingBox: undefined,
|
||||||
|
// constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||||
|
transform: function transform(node, pos) {
|
||||||
|
return pos;
|
||||||
|
},
|
||||||
|
// a function that applies a transform to the final node position
|
||||||
|
ready: function ready() {},
|
||||||
|
// on layoutready
|
||||||
|
sort: undefined,
|
||||||
|
// a sorting function to order the nodes and edges; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
||||||
|
// because cytoscape dagre creates a directed graph, and directed graphs use the node order as a tie breaker when
|
||||||
|
// defining the topology of a graph, this sort function can help ensure the correct order of the nodes/edges.
|
||||||
|
// this feature is most useful when adding and removing the same nodes and edges multiple times in a graph.
|
||||||
|
stop: function stop() {} // on layoutstop
|
||||||
|
|
||||||
|
};
|
||||||
|
module.exports = defaults;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 3 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
// Simple, internal Object.assign() polyfill for options objects etc.
|
||||||
|
module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
|
||||||
|
for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||||
|
srcs[_key - 1] = arguments[_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
srcs.forEach(function (src) {
|
||||||
|
Object.keys(src).forEach(function (k) {
|
||||||
|
return tgt[k] = src[k];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tgt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 4 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
module.exports = __WEBPACK_EXTERNAL_MODULE__4__;
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
/******/ ]);
|
||||||
|
});
|
||||||
32
js/cytoscape.min.js
vendored
Normal file
32
js/cytoscape.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3809
js/dagre.min.js
vendored
Normal file
3809
js/dagre.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
850
js/kundenkarte_cytoscape.js
Normal file
850
js/kundenkarte_cytoscape.js
Normal file
|
|
@ -0,0 +1,850 @@
|
||||||
|
/**
|
||||||
|
* KundenKarte Graph-Ansicht (Cytoscape.js)
|
||||||
|
* Hierarchisches Top-Down Layout (dagre)
|
||||||
|
* Gebäude-Typen als Container, Geräte darin, Kabel als Verbindungen
|
||||||
|
* Keine Pfeile - Stromleitungen haben keine Richtung
|
||||||
|
*
|
||||||
|
* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.KundenKarte = window.KundenKarte || {};
|
||||||
|
|
||||||
|
// FontAwesome 5 Unicode-Mapping für Cytoscape-Labels
|
||||||
|
var FA_ICONS = {
|
||||||
|
'fa-home': '\uf015',
|
||||||
|
'fa-building': '\uf1ad',
|
||||||
|
'fa-warehouse': '\uf494',
|
||||||
|
'fa-compass': '\uf14e',
|
||||||
|
'fa-utensils': '\uf2e7',
|
||||||
|
'fa-bed': '\uf236',
|
||||||
|
'fa-bath': '\uf2cd',
|
||||||
|
'fa-car': '\uf1b9',
|
||||||
|
'fa-car-side': '\uf5e4',
|
||||||
|
'fa-tree': '\uf1bb',
|
||||||
|
'fa-leaf': '\uf06c',
|
||||||
|
'fa-sun': '\uf185',
|
||||||
|
'fa-child': '\uf1ae',
|
||||||
|
'fa-desktop': '\uf108',
|
||||||
|
'fa-server': '\uf233',
|
||||||
|
'fa-cogs': '\uf085',
|
||||||
|
'fa-tools': '\uf7d9',
|
||||||
|
'fa-box': '\uf466',
|
||||||
|
'fa-door-open': '\uf52b',
|
||||||
|
'fa-door-closed': '\uf52a',
|
||||||
|
'fa-square': '\uf0c8',
|
||||||
|
'fa-charging-station': '\uf5e7',
|
||||||
|
'fa-database': '\uf1c0',
|
||||||
|
'fa-fire': '\uf06d',
|
||||||
|
'fa-solar-panel': '\uf5ba',
|
||||||
|
'fa-tachometer-alt': '\uf3fd',
|
||||||
|
'fa-th-list': '\uf00b',
|
||||||
|
'fa-bolt': '\uf0e7',
|
||||||
|
'fa-plus-square': '\uf0fe',
|
||||||
|
'fa-level-down-alt': '\uf3be',
|
||||||
|
'fa-level-up-alt': '\uf3bf',
|
||||||
|
'fa-layer-group': '\uf5fd',
|
||||||
|
'fa-arrows-alt-h': '\uf337',
|
||||||
|
'fa-tshirt': '\uf553',
|
||||||
|
'fa-mountain': '\uf6fc',
|
||||||
|
'fa-horse': '\uf6f0',
|
||||||
|
'fa-toilet': '\uf7d8',
|
||||||
|
'fa-couch': '\uf4b8',
|
||||||
|
'fa-plug': '\uf1e6',
|
||||||
|
'fa-sitemap': '\uf0e8'
|
||||||
|
};
|
||||||
|
|
||||||
|
KundenKarte.Graph = {
|
||||||
|
cy: null,
|
||||||
|
containerId: 'kundenkarte-graph-container',
|
||||||
|
ajaxUrl: '',
|
||||||
|
saveUrl: '',
|
||||||
|
moduleUrl: '',
|
||||||
|
socId: 0,
|
||||||
|
contactId: 0,
|
||||||
|
isContact: false,
|
||||||
|
tooltipEl: null,
|
||||||
|
wheelZoomEnabled: false,
|
||||||
|
hasPositions: false,
|
||||||
|
_saveTimer: null,
|
||||||
|
_dirtyNodes: {},
|
||||||
|
_viewportTimer: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisierung
|
||||||
|
*/
|
||||||
|
init: function(config) {
|
||||||
|
this.socId = config.socId || 0;
|
||||||
|
this.contactId = config.contactId || 0;
|
||||||
|
this.isContact = config.isContact || false;
|
||||||
|
|
||||||
|
this.createTooltipElement();
|
||||||
|
this.bindZoomButtons();
|
||||||
|
this.loadGraphData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FontAwesome-Klasse in Unicode-Zeichen umwandeln
|
||||||
|
*/
|
||||||
|
getIconChar: function(picto) {
|
||||||
|
if (!picto) return '';
|
||||||
|
var parts = picto.split(' ');
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].indexOf('fa-') === 0 && FA_ICONS[parts[i]]) {
|
||||||
|
return FA_ICONS[parts[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltip-Element erzeugen
|
||||||
|
*/
|
||||||
|
createTooltipElement: function() {
|
||||||
|
this.tooltipEl = document.createElement('div');
|
||||||
|
this.tooltipEl.className = 'kundenkarte-graph-tooltip';
|
||||||
|
this.tooltipEl.style.display = 'none';
|
||||||
|
document.body.appendChild(this.tooltipEl);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daten vom Server laden
|
||||||
|
*/
|
||||||
|
loadGraphData: function() {
|
||||||
|
var self = this;
|
||||||
|
var url = this.ajaxUrl + '?socid=' + this.socId;
|
||||||
|
if (this.contactId > 0) {
|
||||||
|
url += '&contactid=' + this.contactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
$('#' + self.containerId + ' .kundenkarte-graph-loading').remove();
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
if (response.elements.nodes.length === 0) {
|
||||||
|
self.showEmpty();
|
||||||
|
} else {
|
||||||
|
self.hasPositions = !!response.has_positions;
|
||||||
|
try {
|
||||||
|
self.renderGraph(response.elements);
|
||||||
|
} catch(e) {
|
||||||
|
console.error('KundenKarte Graph Fehler:', e);
|
||||||
|
}
|
||||||
|
// Legende IMMER rendern (auch bei Graph-Fehler)
|
||||||
|
self.renderLegend(response.cable_types || []);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof KundenKarte.showError === 'function') {
|
||||||
|
KundenKarte.showError('Fehler', response.error || 'Daten konnten nicht geladen werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#' + self.containerId + ' .kundenkarte-graph-loading').remove();
|
||||||
|
if (typeof KundenKarte.showError === 'function') {
|
||||||
|
KundenKarte.showError('Fehler', 'Netzwerkfehler beim Laden der Graph-Daten');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leeren Zustand anzeigen
|
||||||
|
*/
|
||||||
|
showEmpty: function() {
|
||||||
|
$('#' + this.containerId).html(
|
||||||
|
'<div class="kundenkarte-graph-empty">' +
|
||||||
|
'<i class="fa fa-sitemap"></i>' +
|
||||||
|
'<span>Keine Elemente vorhanden</span>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamische Legende rendern (Gebäude + Geräte + Kabeltypen)
|
||||||
|
*/
|
||||||
|
renderLegend: function(cableTypes) {
|
||||||
|
var $legend = $('#kundenkarte-graph-legend');
|
||||||
|
if (!$legend.length) return;
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
// Feste Einträge: Raum + Gerät
|
||||||
|
html += '<span class="kundenkarte-graph-legend-item">';
|
||||||
|
html += '<span class="kundenkarte-graph-legend-box building"></span> Raum/Gebäude</span>';
|
||||||
|
html += '<span class="kundenkarte-graph-legend-item">';
|
||||||
|
html += '<span class="kundenkarte-graph-legend-box device"></span> Gerät</span>';
|
||||||
|
|
||||||
|
// Dynamische Kabeltypen mit Farben
|
||||||
|
if (cableTypes && cableTypes.length > 0) {
|
||||||
|
for (var i = 0; i < cableTypes.length; i++) {
|
||||||
|
var ct = cableTypes[i];
|
||||||
|
html += '<span class="kundenkarte-graph-legend-item">';
|
||||||
|
html += '<span class="kundenkarte-graph-legend-line" style="background:' + ct.color + ';"></span> ';
|
||||||
|
html += this.escapeHtml(ct.label) + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durchgeschleift (immer anzeigen)
|
||||||
|
html += '<span class="kundenkarte-graph-legend-item">';
|
||||||
|
html += '<span class="kundenkarte-graph-legend-line passthrough"></span> Durchgeschleift</span>';
|
||||||
|
|
||||||
|
$legend.html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rastergröße für Snap-to-Grid (unsichtbar)
|
||||||
|
*/
|
||||||
|
gridSize: 20,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximaler Zoom beim Einpassen (verhindert zu starkes Reinzoomen)
|
||||||
|
*/
|
||||||
|
maxFitZoom: 0.85,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LocalStorage-Key für Viewport-State
|
||||||
|
*/
|
||||||
|
_viewportKey: function() {
|
||||||
|
return 'kundenkarte_graph_vp_' + this.socId + '_' + this.contactId;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Viewport (Zoom + Pan) im localStorage speichern
|
||||||
|
*/
|
||||||
|
saveViewport: function() {
|
||||||
|
if (!this.cy) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(this._viewportKey(), JSON.stringify({
|
||||||
|
zoom: this.cy.zoom(),
|
||||||
|
pan: this.cy.pan()
|
||||||
|
}));
|
||||||
|
} catch(e) { /* localStorage voll/nicht verfügbar */ }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Viewport aus localStorage wiederherstellen
|
||||||
|
* @return {boolean} true wenn wiederhergestellt
|
||||||
|
*/
|
||||||
|
restoreViewport: function() {
|
||||||
|
if (!this.cy) return false;
|
||||||
|
try {
|
||||||
|
var saved = localStorage.getItem(this._viewportKey());
|
||||||
|
if (saved) {
|
||||||
|
var vp = JSON.parse(saved);
|
||||||
|
this.cy.viewport({ zoom: vp.zoom, pan: vp.pan });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch(e) { /* ungültige Daten */ }
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph rendern
|
||||||
|
*/
|
||||||
|
renderGraph: function(elements) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Labels zusammenbauen: Name + Klammer-Felder + Badge-Felder
|
||||||
|
var cyElements = [];
|
||||||
|
if (elements.nodes) {
|
||||||
|
elements.nodes.forEach(function(n) {
|
||||||
|
var icon = self.getIconChar(n.data.type_picto);
|
||||||
|
var namePart = icon ? (icon + ' ' + n.data.label) : n.data.label;
|
||||||
|
|
||||||
|
if (n.data.is_building) {
|
||||||
|
// Gebäude: Icon + Name + evtl. Klammer-Felder
|
||||||
|
var parens = [];
|
||||||
|
if (n.data.fields) {
|
||||||
|
for (var i = 0; i < n.data.fields.length; i++) {
|
||||||
|
var f = n.data.fields[i];
|
||||||
|
if (f.display === 'parentheses' && f.value) {
|
||||||
|
parens.push(f.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.data.display_label = parens.length > 0
|
||||||
|
? namePart + ' (' + parens.join(', ') + ')'
|
||||||
|
: namePart;
|
||||||
|
} else {
|
||||||
|
// Gerät: Name (+ Klammer-Felder) + Trennlinie + Badge-Felder
|
||||||
|
var parenParts = [];
|
||||||
|
var badgeLines = [];
|
||||||
|
|
||||||
|
if (n.data.fields) {
|
||||||
|
for (var i = 0; i < n.data.fields.length; i++) {
|
||||||
|
var f = n.data.fields[i];
|
||||||
|
var v = f.value;
|
||||||
|
if (!v || v === '') continue;
|
||||||
|
if (v === '1' && f.display === 'badge') v = '\u2713';
|
||||||
|
|
||||||
|
if (f.display === 'parentheses') {
|
||||||
|
parenParts.push(v);
|
||||||
|
} else if (f.display === 'badge') {
|
||||||
|
badgeLines.push(f.label + ': ' + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = [];
|
||||||
|
// Zeile 1: Name + Klammer-Werte
|
||||||
|
if (parenParts.length > 0) {
|
||||||
|
lines.push(namePart + ' (' + parenParts.join(', ') + ')');
|
||||||
|
} 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.data.display_label = lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
cyElements.push(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (elements.edges) {
|
||||||
|
elements.edges.forEach(function(e) { cyElements.push(e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einpassen, aber nicht über maxFitZoom hinaus zoomen
|
||||||
|
var fitWithLimit = function(cy) {
|
||||||
|
cy.fit(undefined, 50);
|
||||||
|
if (cy.zoom() > self.maxFitZoom) {
|
||||||
|
cy.zoom(self.maxFitZoom);
|
||||||
|
cy.center();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Container-Höhe an Inhalt anpassen
|
||||||
|
var autoResizeContainer = function(cy) {
|
||||||
|
var bb = cy.elements().boundingBox();
|
||||||
|
if (!bb || bb.w === 0) return;
|
||||||
|
// Benötigte Höhe bei aktuellem Zoom + Padding
|
||||||
|
var neededHeight = (bb.h * cy.zoom()) + 100;
|
||||||
|
var container = document.getElementById(self.containerId);
|
||||||
|
var minH = 300;
|
||||||
|
var maxH = window.innerHeight * 0.8;
|
||||||
|
var newH = Math.max(minH, Math.min(maxH, neededHeight));
|
||||||
|
if (Math.abs(newH - container.clientHeight) > 20) {
|
||||||
|
container.style.height = Math.round(newH) + 'px';
|
||||||
|
cy.resize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cytoscape-Instanz erstellen (ohne Layout - wird separat gestartet)
|
||||||
|
this.cy = cytoscape({
|
||||||
|
container: document.getElementById(this.containerId),
|
||||||
|
elements: cyElements,
|
||||||
|
style: this.getStylesheet(),
|
||||||
|
minZoom: 0.15,
|
||||||
|
maxZoom: 4,
|
||||||
|
userZoomingEnabled: false,
|
||||||
|
userPanningEnabled: true,
|
||||||
|
boxSelectionEnabled: false,
|
||||||
|
layout: { name: 'preset' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// dagre-Layout NACH Instanz-Erstellung starten (self.cy ist jetzt gesetzt)
|
||||||
|
this.cy.layout({
|
||||||
|
name: 'dagre',
|
||||||
|
rankDir: 'TB',
|
||||||
|
nodeSep: 50,
|
||||||
|
rankSep: 80,
|
||||||
|
fit: false,
|
||||||
|
stop: function() {
|
||||||
|
if (self.hasPositions) {
|
||||||
|
// Gespeicherte Positionen anwenden
|
||||||
|
self.cy.nodes().forEach(function(node) {
|
||||||
|
var d = node.data();
|
||||||
|
if (d.graph_x !== null && d.graph_y !== null) {
|
||||||
|
node.position({ x: d.graph_x, y: d.graph_y });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Container an Inhalt anpassen, dann Viewport wiederherstellen
|
||||||
|
autoResizeContainer(self.cy);
|
||||||
|
if (!self.restoreViewport()) {
|
||||||
|
fitWithLimit(self.cy);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Kein gespeichertes Layout: dagre-Ergebnis einpassen
|
||||||
|
autoResizeContainer(self.cy);
|
||||||
|
fitWithLimit(self.cy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
this.bindGraphEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cytoscape Stylesheet
|
||||||
|
*/
|
||||||
|
getStylesheet: function() {
|
||||||
|
var faFont = '"Font Awesome 5 Free", "FontAwesome", sans-serif';
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Räume/Gebäude (Compound-Container)
|
||||||
|
{
|
||||||
|
selector: '.building-node',
|
||||||
|
style: {
|
||||||
|
'shape': 'roundrectangle',
|
||||||
|
'background-color': '#2a2b2d',
|
||||||
|
'background-opacity': 1,
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#4390dc',
|
||||||
|
'border-style': 'dashed',
|
||||||
|
'padding': '25px',
|
||||||
|
'label': 'data(display_label)',
|
||||||
|
'font-family': faFont,
|
||||||
|
'font-weight': 'bold',
|
||||||
|
'text-valign': 'top',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'font-size': '13px',
|
||||||
|
'color': '#ffffff',
|
||||||
|
'text-margin-y': -8,
|
||||||
|
'text-background-color': '#2a2b2d',
|
||||||
|
'text-background-opacity': 0.95,
|
||||||
|
'text-background-padding': '4px',
|
||||||
|
'text-background-shape': 'roundrectangle',
|
||||||
|
'min-width': '120px',
|
||||||
|
'min-height': '60px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Geräte - Karten-Design mit Feldwerten
|
||||||
|
{
|
||||||
|
selector: '.device-node',
|
||||||
|
style: {
|
||||||
|
'shape': 'roundrectangle',
|
||||||
|
'width': 'label',
|
||||||
|
'height': 'label',
|
||||||
|
'padding': '14px',
|
||||||
|
'background-color': '#2d4a3a',
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#5a9a6a',
|
||||||
|
'label': 'data(display_label)',
|
||||||
|
'font-family': faFont,
|
||||||
|
'font-weight': 'bold',
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'text-justification': 'left',
|
||||||
|
'font-size': '11px',
|
||||||
|
'color': '#ffffff',
|
||||||
|
'text-wrap': 'wrap',
|
||||||
|
'text-max-width': '220px',
|
||||||
|
'line-height': 1.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Kabel - Farbe aus medium_color, Fallback grün
|
||||||
|
{
|
||||||
|
selector: '.cable-edge',
|
||||||
|
style: {
|
||||||
|
'width': 3,
|
||||||
|
'line-color': function(edge) {
|
||||||
|
return edge.data('medium_color') || '#5a8a5a';
|
||||||
|
},
|
||||||
|
'target-arrow-shape': 'none',
|
||||||
|
'source-arrow-shape': 'none',
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
'label': 'data(label)',
|
||||||
|
'font-size': '9px',
|
||||||
|
'color': '#8a9aa8',
|
||||||
|
'text-rotation': 'autorotate',
|
||||||
|
'text-background-color': '#1a2030',
|
||||||
|
'text-background-opacity': 0.85,
|
||||||
|
'text-background-padding': '3px',
|
||||||
|
'text-background-shape': 'roundrectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Durchgeschleifte Leitungen - gestrichelt
|
||||||
|
{
|
||||||
|
selector: '.passthrough-edge',
|
||||||
|
style: {
|
||||||
|
'width': 1,
|
||||||
|
'line-color': '#505860',
|
||||||
|
'line-style': 'dashed',
|
||||||
|
'target-arrow-shape': 'none',
|
||||||
|
'source-arrow-shape': 'none',
|
||||||
|
'curve-style': 'bezier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Hover
|
||||||
|
{
|
||||||
|
selector: 'node:active',
|
||||||
|
style: {
|
||||||
|
'overlay-opacity': 0.1,
|
||||||
|
'overlay-color': '#7ab0d4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Selektiert
|
||||||
|
{
|
||||||
|
selector: ':selected',
|
||||||
|
style: {
|
||||||
|
'border-color': '#d4944a',
|
||||||
|
'border-width': 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph-Events binden
|
||||||
|
*/
|
||||||
|
bindGraphEvents: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Tap auf Geräte-Node -> Detail-Ansicht
|
||||||
|
this.cy.on('tap', 'node.device-node', function(evt) {
|
||||||
|
var node = evt.target;
|
||||||
|
var anlageId = node.data('id').replace('n_', '');
|
||||||
|
var url;
|
||||||
|
if (self.isContact && self.contactId > 0) {
|
||||||
|
url = self.moduleUrl + '/tabs/contact_anlagen.php?id=' + self.contactId
|
||||||
|
+ '&action=view&anlage_id=' + anlageId;
|
||||||
|
} else {
|
||||||
|
url = self.moduleUrl + '/tabs/anlagen.php?id=' + self.socId
|
||||||
|
+ '&action=view&anlage_id=' + anlageId;
|
||||||
|
}
|
||||||
|
window.location.href = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tap auf Kabel-Edge -> Verbindungs-Editor
|
||||||
|
this.cy.on('tap', 'edge.cable-edge', function(evt) {
|
||||||
|
var edge = evt.target;
|
||||||
|
var connId = edge.data('connection_id');
|
||||||
|
if (connId) {
|
||||||
|
window.location.href = self.moduleUrl + '/anlage_connection.php?id=' + connId + '&action=edit';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mouseover Tooltip
|
||||||
|
this.cy.on('mouseover', 'node', function(evt) {
|
||||||
|
self.showNodeTooltip(evt.target, evt.renderedPosition);
|
||||||
|
});
|
||||||
|
this.cy.on('mouseout', 'node', function() {
|
||||||
|
self.hideTooltip();
|
||||||
|
});
|
||||||
|
this.cy.on('mouseover', 'edge.cable-edge', function(evt) {
|
||||||
|
self.showEdgeTooltip(evt.target, evt.renderedPosition);
|
||||||
|
});
|
||||||
|
this.cy.on('mouseout', 'edge', function() {
|
||||||
|
self.hideTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag: Begrenzung auf sichtbaren Bereich
|
||||||
|
this.cy.on('drag', 'node', function(evt) {
|
||||||
|
var node = evt.target;
|
||||||
|
var pos = node.position();
|
||||||
|
var ext = self.cy.extent();
|
||||||
|
// Etwas Rand lassen damit Node nicht am Rand klebt
|
||||||
|
var margin = 20;
|
||||||
|
var clampedX = Math.max(ext.x1 + margin, Math.min(ext.x2 - margin, pos.x));
|
||||||
|
var clampedY = Math.max(ext.y1 + margin, Math.min(ext.y2 - margin, pos.y));
|
||||||
|
if (clampedX !== pos.x || clampedY !== pos.y) {
|
||||||
|
node.position({ x: clampedX, y: clampedY });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag: Snap-to-Grid + Position merken (inkl. Kinder bei Compound-Nodes)
|
||||||
|
this.cy.on('dragfree', 'node', function(evt) {
|
||||||
|
var node = evt.target;
|
||||||
|
var pos = node.position();
|
||||||
|
var grid = self.gridSize;
|
||||||
|
|
||||||
|
// Auf Raster einrasten
|
||||||
|
var snappedX = Math.round(pos.x / grid) * grid;
|
||||||
|
var snappedY = Math.round(pos.y / grid) * grid;
|
||||||
|
node.position({ x: snappedX, y: snappedY });
|
||||||
|
|
||||||
|
// Position zum Speichern vormerken
|
||||||
|
var anlageId = node.data('id').replace('n_', '');
|
||||||
|
self._dirtyNodes[anlageId] = { id: parseInt(anlageId), x: snappedX, y: snappedY };
|
||||||
|
|
||||||
|
// Bei Compound-Nodes auch alle Kinder speichern
|
||||||
|
var children = node.children();
|
||||||
|
if (children.length > 0) {
|
||||||
|
children.forEach(function(child) {
|
||||||
|
var childPos = child.position();
|
||||||
|
var childId = child.data('id').replace('n_', '');
|
||||||
|
self._dirtyNodes[childId] = { id: parseInt(childId), x: childPos.x, y: childPos.y };
|
||||||
|
// Rekursiv für verschachtelte Compounds
|
||||||
|
child.children().forEach(function(grandchild) {
|
||||||
|
var gcPos = grandchild.position();
|
||||||
|
var gcId = grandchild.data('id').replace('n_', '');
|
||||||
|
self._dirtyNodes[gcId] = { id: parseInt(gcId), x: gcPos.x, y: gcPos.y };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced speichern (500ms nach letztem Drag)
|
||||||
|
if (self._saveTimer) clearTimeout(self._saveTimer);
|
||||||
|
self._saveTimer = setTimeout(function() {
|
||||||
|
self.savePositions();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Viewport-Änderungen speichern (Pan + Zoom)
|
||||||
|
this.cy.on('viewport', function() {
|
||||||
|
if (self._viewportTimer) clearTimeout(self._viewportTimer);
|
||||||
|
self._viewportTimer = setTimeout(function() {
|
||||||
|
self.saveViewport();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
this.cy.on('mouseover', 'node.device-node, edge.cable-edge', function() {
|
||||||
|
document.getElementById(self.containerId).style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
this.cy.on('mouseout', 'node, edge', function() {
|
||||||
|
document.getElementById(self.containerId).style.cursor = 'default';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom-Buttons binden
|
||||||
|
*/
|
||||||
|
bindZoomButtons: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Mausrad-Zoom Ein/Aus
|
||||||
|
$(document).on('click', '#btn-graph-wheel-zoom', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self.wheelZoomEnabled = !self.wheelZoomEnabled;
|
||||||
|
if (self.cy) {
|
||||||
|
self.cy.userZoomingEnabled(self.wheelZoomEnabled);
|
||||||
|
}
|
||||||
|
$(this).toggleClass('active', self.wheelZoomEnabled);
|
||||||
|
$(this).attr('title', self.wheelZoomEnabled ? 'Mausrad-Zoom aus' : 'Mausrad-Zoom ein');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#btn-graph-fit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (self.cy) {
|
||||||
|
self.cy.zoom(self.maxFitZoom);
|
||||||
|
self.cy.center();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(document).on('click', '#btn-graph-zoom-in', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (self.cy) {
|
||||||
|
self.cy.zoom({
|
||||||
|
level: self.cy.zoom() * 1.3,
|
||||||
|
renderedPosition: { x: self.cy.width() / 2, y: self.cy.height() / 2 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(document).on('click', '#btn-graph-zoom-out', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (self.cy) {
|
||||||
|
self.cy.zoom({
|
||||||
|
level: self.cy.zoom() / 1.3,
|
||||||
|
renderedPosition: { x: self.cy.width() / 2, y: self.cy.height() / 2 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layout zurücksetzen
|
||||||
|
$(document).on('click', '#btn-graph-reset-layout', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self.resetLayout();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geänderte Positionen an Server senden
|
||||||
|
*/
|
||||||
|
savePositions: function() {
|
||||||
|
var positions = [];
|
||||||
|
for (var id in this._dirtyNodes) {
|
||||||
|
positions.push(this._dirtyNodes[id]);
|
||||||
|
}
|
||||||
|
if (positions.length === 0) return;
|
||||||
|
|
||||||
|
// Dirty-Liste leeren
|
||||||
|
this._dirtyNodes = {};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: this.saveUrl + '?action=save',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ positions: positions }),
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(resp) {
|
||||||
|
if (!resp.success) {
|
||||||
|
console.error('Graph-Positionen speichern fehlgeschlagen:', resp.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, err) {
|
||||||
|
console.error('Graph-Positionen Netzwerkfehler:', status, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout zurücksetzen: Positionen löschen + dagre neu berechnen
|
||||||
|
*/
|
||||||
|
resetLayout: function() {
|
||||||
|
if (!this.cy) return;
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Positionen in DB + Viewport in localStorage löschen
|
||||||
|
$.ajax({
|
||||||
|
url: this.saveUrl + '?action=reset&socid=' + this.socId +
|
||||||
|
(this.contactId > 0 ? '&contactid=' + this.contactId : ''),
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json'
|
||||||
|
});
|
||||||
|
try { localStorage.removeItem(this._viewportKey()); } catch(e) {}
|
||||||
|
|
||||||
|
// dagre Layout neu berechnen
|
||||||
|
this.hasPositions = false;
|
||||||
|
this._dirtyNodes = {};
|
||||||
|
var cy = this.cy;
|
||||||
|
var maxZoom = this.maxFitZoom;
|
||||||
|
cy.layout({
|
||||||
|
name: 'dagre',
|
||||||
|
rankDir: 'TB',
|
||||||
|
nodeSep: 50,
|
||||||
|
rankSep: 80,
|
||||||
|
fit: false,
|
||||||
|
animate: true,
|
||||||
|
animationDuration: 400,
|
||||||
|
stop: function() {
|
||||||
|
// Container an neues Layout anpassen
|
||||||
|
var bb = cy.elements().boundingBox();
|
||||||
|
if (bb && bb.h > 0) {
|
||||||
|
var neededH = (bb.h * maxZoom) + 100;
|
||||||
|
var container = document.getElementById(self.containerId);
|
||||||
|
var minH = 300;
|
||||||
|
var maxH = window.innerHeight * 0.8;
|
||||||
|
var newH = Math.max(minH, Math.min(maxH, neededH));
|
||||||
|
container.style.height = Math.round(newH) + 'px';
|
||||||
|
cy.resize();
|
||||||
|
}
|
||||||
|
cy.fit(undefined, 50);
|
||||||
|
if (cy.zoom() > maxZoom) {
|
||||||
|
cy.zoom(maxZoom);
|
||||||
|
cy.center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).run();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node-Tooltip
|
||||||
|
*/
|
||||||
|
showNodeTooltip: function(node, position) {
|
||||||
|
var data = node.data();
|
||||||
|
// Überschrift: Icon + Bezeichnung
|
||||||
|
var 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>';
|
||||||
|
|
||||||
|
if (data.fields && data.fields.length > 0) {
|
||||||
|
for (var i = 0; i < data.fields.length && i < 10; i++) {
|
||||||
|
var f = data.fields[i];
|
||||||
|
if (!f.value || f.value === '') continue;
|
||||||
|
var val = f.value;
|
||||||
|
if (val === '1') val = '\u2713';
|
||||||
|
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>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="tooltip-field-value">' + this.escapeHtml(String(val)) + '</span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltipEl.innerHTML = html;
|
||||||
|
this.tooltipEl.style.display = 'block';
|
||||||
|
this.positionTooltip(position);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge-Tooltip
|
||||||
|
*/
|
||||||
|
showEdgeTooltip: function(edge, position) {
|
||||||
|
var data = edge.data();
|
||||||
|
var html = '<div class="tooltip-title">Verbindung</div>';
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
if (data.medium_spec) {
|
||||||
|
html += '<div class="tooltip-field"><span class="tooltip-field-label">Spezifikation</span><span class="tooltip-field-value">' + this.escapeHtml(data.medium_spec) + '</span></div>';
|
||||||
|
}
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
this.tooltipEl.innerHTML = html;
|
||||||
|
this.tooltipEl.style.display = 'block';
|
||||||
|
this.positionTooltip(position);
|
||||||
|
},
|
||||||
|
|
||||||
|
positionTooltip: function(renderedPos) {
|
||||||
|
var container = document.getElementById(this.containerId);
|
||||||
|
var rect = container.getBoundingClientRect();
|
||||||
|
var x = rect.left + renderedPos.x + 15;
|
||||||
|
var y = rect.top + renderedPos.y - 10 + window.scrollY;
|
||||||
|
if (x + 300 > window.innerWidth) {
|
||||||
|
x = rect.left + renderedPos.x - 315;
|
||||||
|
}
|
||||||
|
this.tooltipEl.style.left = x + 'px';
|
||||||
|
this.tooltipEl.style.top = y + 'px';
|
||||||
|
},
|
||||||
|
|
||||||
|
hideTooltip: function() {
|
||||||
|
if (this.tooltipEl) this.tooltipEl.style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
escapeHtml: function(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
if (this.cy) { this.cy.destroy(); this.cy = null; }
|
||||||
|
if (this.tooltipEl && this.tooltipEl.parentNode) {
|
||||||
|
this.tooltipEl.parentNode.removeChild(this.tooltipEl);
|
||||||
|
this.tooltipEl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-Init
|
||||||
|
$(document).ready(function() {
|
||||||
|
var container = document.getElementById('kundenkarte-graph-container');
|
||||||
|
if (container) {
|
||||||
|
KundenKarte.Graph.ajaxUrl = container.getAttribute('data-ajax-url') || '';
|
||||||
|
KundenKarte.Graph.saveUrl = container.getAttribute('data-save-url') || '';
|
||||||
|
KundenKarte.Graph.moduleUrl = container.getAttribute('data-module-url') || '';
|
||||||
|
KundenKarte.Graph.init({
|
||||||
|
socId: parseInt(container.getAttribute('data-socid')) || 0,
|
||||||
|
contactId: parseInt(container.getAttribute('data-contactid')) || 0,
|
||||||
|
isContact: container.hasAttribute('data-contactid') && parseInt(container.getAttribute('data-contactid')) > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
5230
js/layout-base.js
Normal file
5230
js/layout-base.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -322,6 +322,19 @@ ErrorNoFileSelected = Keine Datei ausgewaehlt
|
||||||
PDFTemplateHelp = Laden Sie eine PDF-Datei als Hintergrund/Briefpapier fuer den Export hoch. Die erste Seite wird als Vorlage verwendet.
|
PDFTemplateHelp = Laden Sie eine PDF-Datei als Hintergrund/Briefpapier fuer den Export hoch. Die erste Seite wird als Vorlage verwendet.
|
||||||
ExportTreeAsPDF = Als PDF exportieren
|
ExportTreeAsPDF = Als PDF exportieren
|
||||||
|
|
||||||
|
# View Mode
|
||||||
|
DefaultViewMode = Standard-Ansichtsmodus fuer Anlagen
|
||||||
|
ViewModeTree = Baumansicht (klassisch)
|
||||||
|
ViewModeGraph = Graph-Ansicht (Cytoscape)
|
||||||
|
SpatialView = Raeumlich
|
||||||
|
TechnicalView = Technisch
|
||||||
|
GraphLoading = Graph wird geladen...
|
||||||
|
GraphLegendRoom = Raum/Gebaeude
|
||||||
|
GraphLegendDevice = Geraet
|
||||||
|
GraphLegendCable = Kabel
|
||||||
|
GraphLegendPassthrough = Durchgeschleift
|
||||||
|
GraphLegendHierarchy = Hierarchie
|
||||||
|
|
||||||
# Tree Display Settings
|
# Tree Display Settings
|
||||||
TreeDisplaySettings = Baum-Anzeige Einstellungen
|
TreeDisplaySettings = Baum-Anzeige Einstellungen
|
||||||
TreeInfoDisplayMode = Zusatzinfos-Anzeige
|
TreeInfoDisplayMode = Zusatzinfos-Anzeige
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,19 @@ ErrorNoFileSelected = No file selected
|
||||||
PDFTemplateHelp = Upload a PDF file as background/letterhead for export. The first page will be used as template.
|
PDFTemplateHelp = Upload a PDF file as background/letterhead for export. The first page will be used as template.
|
||||||
ExportTreeAsPDF = Export as PDF
|
ExportTreeAsPDF = Export as PDF
|
||||||
|
|
||||||
|
# View Mode
|
||||||
|
DefaultViewMode = Default view mode for installations
|
||||||
|
ViewModeTree = Tree view (classic)
|
||||||
|
ViewModeGraph = Graph view (Cytoscape)
|
||||||
|
SpatialView = Spatial
|
||||||
|
TechnicalView = Technical
|
||||||
|
GraphLoading = Loading graph...
|
||||||
|
GraphLegendRoom = Room/Building
|
||||||
|
GraphLegendDevice = Device
|
||||||
|
GraphLegendCable = Cable
|
||||||
|
GraphLegendPassthrough = Passthrough
|
||||||
|
GraphLegendHierarchy = Hierarchy
|
||||||
|
|
||||||
# Tree Display Settings
|
# Tree Display Settings
|
||||||
TreeDisplaySettings = Tree Display Settings
|
TreeDisplaySettings = Tree Display Settings
|
||||||
TreeInfoDisplayMode = Info Display Mode
|
TreeInfoDisplayMode = Info Display Mode
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ CREATE TABLE llx_kundenkarte_anlage
|
||||||
fk_parent integer DEFAULT 0 NOT NULL,
|
fk_parent integer DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
fk_system integer NOT NULL,
|
fk_system integer NOT NULL,
|
||||||
|
fk_building_node integer DEFAULT 0,
|
||||||
|
|
||||||
manufacturer varchar(128),
|
manufacturer varchar(128),
|
||||||
model varchar(128),
|
model varchar(128),
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,24 @@ if ($action == 'togglepin' && $permissiontoadd) {
|
||||||
// Use Dolibarr standard button classes
|
// Use Dolibarr standard button classes
|
||||||
|
|
||||||
$title = $langs->trans('TechnicalInstallations').' - '.$object->name;
|
$title = $langs->trans('TechnicalInstallations').' - '.$object->name;
|
||||||
llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/pathfinding.min.js', '/kundenkarte/js/kundenkarte.js?v='.time()), array('/kundenkarte/css/kundenkarte.css?v='.time()));
|
|
||||||
|
// Ansichtsmodus (Admin-Setting)
|
||||||
|
$viewMode = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
|
||||||
|
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
|
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
|
||||||
|
|
||||||
// Prepare tabs
|
// Prepare tabs
|
||||||
$head = societe_prepare_head($object);
|
$head = societe_prepare_head($object);
|
||||||
|
|
@ -452,6 +469,24 @@ print '</div>';
|
||||||
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
|
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
|
||||||
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
||||||
if ($isTreeView) {
|
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='.$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 {
|
||||||
print '<div class="kundenkarte-tree-controls">';
|
print '<div class="kundenkarte-tree-controls">';
|
||||||
// Compact mode toggle (visible on mobile)
|
// Compact mode toggle (visible on mobile)
|
||||||
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">';
|
||||||
|
|
@ -470,6 +505,7 @@ if ($isTreeView) {
|
||||||
print '</a>';
|
print '</a>';
|
||||||
}
|
}
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
||||||
|
|
@ -991,8 +1027,8 @@ if (empty($customerSystems)) {
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Tree view
|
// Listenansicht (Baum oder Graph)
|
||||||
if ($permissiontoadd) {
|
if ($permissiontoadd && $viewMode !== 'graph') {
|
||||||
print '<div style="margin-bottom:15px;">';
|
print '<div style="margin-bottom:15px;">';
|
||||||
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
|
||||||
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
|
||||||
|
|
@ -1000,6 +1036,28 @@ if (empty($customerSystems)) {
|
||||||
print '</div>';
|
print '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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="'.$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>';
|
||||||
|
} else {
|
||||||
|
// Baumansicht (klassisch)
|
||||||
|
|
||||||
// Load tree
|
// Load tree
|
||||||
$tree = $anlage->fetchTree($id, $systemId);
|
$tree = $anlage->fetchTree($id, $systemId);
|
||||||
|
|
||||||
|
|
@ -1038,6 +1096,7 @@ if (empty($customerSystems)) {
|
||||||
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
|
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,24 @@ if ($action == 'togglepin' && $permissiontoadd) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs);
|
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs);
|
||||||
llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/pathfinding.min.js', '/kundenkarte/js/kundenkarte.js?v='.time()), array('/kundenkarte/css/kundenkarte.css?v='.time()));
|
|
||||||
|
// Ansichtsmodus (Admin-Setting)
|
||||||
|
$viewMode = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
|
||||||
|
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
|
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
|
||||||
|
|
||||||
// Prepare tabs
|
// Prepare tabs
|
||||||
$head = contact_prepare_head($object);
|
$head = contact_prepare_head($object);
|
||||||
|
|
@ -451,6 +468,24 @@ print '</div>';
|
||||||
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
|
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
|
||||||
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
||||||
if ($isTreeView) {
|
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 {
|
||||||
print '<div class="kundenkarte-tree-controls">';
|
print '<div class="kundenkarte-tree-controls">';
|
||||||
// Compact mode toggle (visible on mobile)
|
// Compact mode toggle (visible on mobile)
|
||||||
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">';
|
||||||
|
|
@ -469,6 +504,7 @@ if ($isTreeView) {
|
||||||
print '</a>';
|
print '</a>';
|
||||||
}
|
}
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
||||||
|
|
@ -990,8 +1026,8 @@ if (empty($customerSystems)) {
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Tree view
|
// Listenansicht (Baum oder Graph)
|
||||||
if ($permissiontoadd) {
|
if ($permissiontoadd && $viewMode !== 'graph') {
|
||||||
print '<div style="margin-bottom:15px;">';
|
print '<div style="margin-bottom:15px;">';
|
||||||
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
|
||||||
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
|
||||||
|
|
@ -999,6 +1035,29 @@ if (empty($customerSystems)) {
|
||||||
print '</div>';
|
print '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>';
|
||||||
|
} else {
|
||||||
|
// Baumansicht (klassisch)
|
||||||
|
|
||||||
// Load tree for this contact
|
// Load tree for this contact
|
||||||
$tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId);
|
$tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId);
|
||||||
|
|
||||||
|
|
@ -1037,6 +1096,7 @@ if (empty($customerSystems)) {
|
||||||
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
|
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
@ -1146,7 +1206,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
|
||||||
$mainText = $conn->label ? $conn->label : $cableInfo;
|
$mainText = $conn->label ? $conn->label : $cableInfo;
|
||||||
$badgeText = $conn->label ? $cableInfo : '';
|
$badgeText = $conn->label ? $cableInfo : '';
|
||||||
|
|
||||||
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&system_id='.$systemId;
|
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&contactid='.$id.'&system_id='.$systemId;
|
||||||
print '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn">';
|
print '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn">';
|
||||||
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
|
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
|
||||||
if ($mainText) {
|
if ($mainText) {
|
||||||
|
|
@ -1394,7 +1454,7 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
|
||||||
$mainText = $conn->label ? $conn->label : $cableInfo;
|
$mainText = $conn->label ? $conn->label : $cableInfo;
|
||||||
$badgeText = $conn->label ? $cableInfo : '';
|
$badgeText = $conn->label ? $cableInfo : '';
|
||||||
|
|
||||||
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&system_id='.$systemId;
|
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&contactid='.$id.'&system_id='.$systemId;
|
||||||
print '<div class="kundenkarte-tree-row">';
|
print '<div class="kundenkarte-tree-row">';
|
||||||
|
|
||||||
// Draw vertical line columns (for cables passing through)
|
// Draw vertical line columns (for cables passing through)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue