diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index 678403a..eb2535f 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -148,25 +148,10 @@ switch ($action) { $db->free($resFields); } - // Root-Anlagen ohne Kontaktzuweisung (Kunden-Ebene) + // Kompletter Baum ohne Kontaktzuweisung (Kunden-Ebene) $anlage = new Anlage($db); - $anlagen = $anlage->fetchChildren(0, $customerId); - - $result = array(); - foreach ($anlagen as $a) { - $item = array( - 'id' => $a->id, - 'label' => $a->label, - 'type' => $a->type_label, - 'has_editor' => !empty($a->schematic_editor_enabled) - ); - // Feld-Badges hinzufügen - $fields = pwaGetAnlageFields($a, $fieldMeta); - if (!empty($fields)) { - $item['fields'] = $fields; - } - $result[] = $item; - } + $tree = $anlage->fetchTree($customerId, 0); + $result = pwaTreeToArray($tree, $fieldMeta); // Kontakt-Adressen mit Anlagen laden $contacts = array(); @@ -229,22 +214,8 @@ switch ($action) { } $anlage = new Anlage($db); - $anlagen = $anlage->fetchChildrenByContact(0, $customerId, $contactId); - - $result = array(); - foreach ($anlagen as $a) { - $item = array( - 'id' => $a->id, - 'label' => $a->label, - 'type' => $a->type_label, - 'has_editor' => !empty($a->schematic_editor_enabled) - ); - $fields = pwaGetAnlageFields($a, $fieldMeta); - if (!empty($fields)) { - $item['fields'] = $fields; - } - $result[] = $item; - } + $tree = $anlage->fetchTreeByContact($customerId, $contactId, 0); + $result = pwaTreeToArray($tree, $fieldMeta); $response['success'] = true; $response['anlagen'] = $result; @@ -931,3 +902,37 @@ function pwaGetAnlageFields($anlage, $fieldMeta) { } return $result; } + +/** + * Anlagen-Baum rekursiv in JSON-Array umwandeln + * + * @param array $nodes Array von Anlage-Objekten mit ->children + * @param array $fieldMeta Feld-Metadaten [typeId][code] = {label, display, color} + * @return array JSON-serialisierbares Array mit children + */ +function pwaTreeToArray($nodes, $fieldMeta) { + $result = array(); + foreach ($nodes as $node) { + $item = array( + 'id' => $node->id, + 'label' => $node->label, + 'type' => $node->type_label, + 'can_have_equipment' => !empty($node->type_can_have_equipment), + 'can_have_children' => !empty($node->type_can_have_children), + ); + + // Feld-Badges + $fields = pwaGetAnlageFields($node, $fieldMeta); + if (!empty($fields)) { + $item['fields'] = $fields; + } + + // Kinder rekursiv + if (!empty($node->children)) { + $item['children'] = pwaTreeToArray($node->children, $fieldMeta); + } + + $result[] = $item; + } + return $result; +} diff --git a/class/anlage.class.php b/class/anlage.class.php index 603c4df..6fd3c75 100755 --- a/class/anlage.class.php +++ b/class/anlage.class.php @@ -227,6 +227,8 @@ class Anlage extends CommonObject $this->type_label = $obj->type_label; $this->type_short = $obj->type_short; $this->type_picto = $obj->type_picto; + $this->type_can_have_children = isset($obj->type_can_have_children) ? (int) $obj->type_can_have_children : 0; + $this->type_can_have_equipment = isset($obj->type_can_have_equipment) ? (int) $obj->type_can_have_equipment : 0; // System info $this->system_label = $obj->system_label; @@ -400,6 +402,7 @@ class Anlage extends CommonObject $results = array(); $sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,"; + $sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,"; $sql .= " s.label as system_label, s.code as system_code,"; // Count images $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,"; @@ -709,6 +712,7 @@ class Anlage extends CommonObject $results = array(); $sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,"; + $sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,"; $sql .= " s.label as system_label, s.code as system_code,"; // Count images $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,"; diff --git a/css/kundenkarte.css b/css/kundenkarte.css index c01dbfb..837f288 100755 --- a/css/kundenkarte.css +++ b/css/kundenkarte.css @@ -336,6 +336,31 @@ body.kundenkarte-drag-active * { font-weight: normal !important; } +/* Tree - Typ-Kategorie Farben */ +/* Gebäudeteile (Struktur): grüner Akzent links */ +.node-structure > .kundenkarte-tree-item { + border-left: 3px solid #4caf50 !important; +} +.node-structure > .kundenkarte-tree-item .kundenkarte-tree-icon { + color: #4caf50 !important; +} + +/* Geräte-Container (can_have_equipment): blauer Akzent links */ +.node-equipment > .kundenkarte-tree-item { + border-left: 3px solid #2196f3 !important; +} +.node-equipment > .kundenkarte-tree-item .kundenkarte-tree-icon { + color: #2196f3 !important; +} + +/* Endgeräte/Blätter: oranger Akzent links */ +.node-leaf > .kundenkarte-tree-item { + border-left: 3px solid #ff9800 !important; +} +.node-leaf > .kundenkarte-tree-item .kundenkarte-tree-icon { + color: #ff9800 !important; +} + /* Tree - File Indicators */ .kundenkarte-tree-files { display: inline-flex !important; diff --git a/css/pwa.css b/css/pwa.css index 459a105..15f7063 100644 --- a/css/pwa.css +++ b/css/pwa.css @@ -438,14 +438,14 @@ body { } /* ============================================ - ANLAGEN GRID + ANLAGEN BAUM ============================================ */ .anlagen-grid { display: flex; flex-direction: column; - gap: 6px; - padding: 12px; + gap: 0; + padding: 8px 0; } /* Trennlabel Kunden-Adresse */ @@ -453,83 +453,132 @@ body { font-size: 12px; font-weight: 600; color: var(--colortextmuted); - padding: 8px 4px 2px; + padding: 8px 12px 2px; border-top: 1px solid var(--colorborder); margin-top: 4px; } -.anlage-card { +/* Baum-Knoten */ +.pwa-tree-node { + /* Container für Knoten + Kinder */ +} + +.pwa-tree-row { display: flex; - flex-direction: row; align-items: center; - gap: 12px; - padding: 14px; - background: var(--colorbackline); - border: 1px solid var(--colorborder); - border-radius: 8px; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--colorborder); cursor: pointer; - transition: all 0.2s; + transition: background 0.15s; } -.anlage-card:active { +.pwa-tree-row:active { background: var(--colorbackinput); - transform: scale(0.98); } -.anlage-card-icon { - width: 44px; - height: 44px; - background: var(--success); - border-radius: 10px; +/* Toggle-Chevron */ +.pwa-tree-toggle { + width: 20px; + height: 20px; + fill: var(--colortextmuted); + flex-shrink: 0; + transition: transform 0.2s; +} + +.pwa-tree-toggle-spacer { + width: 20px; + flex-shrink: 0; +} + +.pwa-tree-node.expanded > .pwa-tree-row > .pwa-tree-toggle { + transform: rotate(90deg); +} + +/* Icon je nach Typ */ +.pwa-tree-icon { + width: 32px; + height: 32px; + border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } -.anlage-card-icon svg { - width: 24px; - height: 24px; +.pwa-tree-icon svg { + width: 18px; + height: 18px; fill: #fff; } -.anlage-card-title { - font-size: 15px; +.pwa-tree-icon.node-structure { + background: #4caf50; +} + +.pwa-tree-icon.node-equipment { + background: #2196f3; +} + +.pwa-tree-icon.node-leaf { + background: #ff9800; +} + +/* Inhalt */ +.pwa-tree-content { + flex: 1; + min-width: 0; +} + +.pwa-tree-label { + font-size: 14px; font-weight: 600; word-break: break-word; line-height: 1.3; } -.anlage-card-content { - flex: 1; - min-width: 0; -} - -.anlage-card-type { - font-size: 12px; +.pwa-tree-type { + font-size: 11px; color: var(--colortextmuted); - margin-top: 2px; + margin-top: 1px; } -.anlage-card-arrow { - width: 20px; - height: 20px; +/* Editor-Pfeil für Equipment-Container */ +.pwa-tree-open { + width: 24px; + height: 24px; fill: var(--colortextmuted); flex-shrink: 0; } +.node-equipment > .pwa-tree-row .pwa-tree-open { + fill: #2196f3; +} + +/* Kinder (eingeklappt) */ +.pwa-tree-children { + display: none; + border-left: 2px solid var(--colorborder); + margin-left: 22px; +} + +.pwa-tree-node.expanded > .pwa-tree-children { + display: block; +} + +/* Feld-Badges (werden wiederverwendet) */ .anlage-card-fields { display: flex; flex-wrap: wrap; gap: 4px; - margin-top: 6px; + margin-top: 4px; } .anlage-field-badge { - font-size: 11px; + font-size: 10px; font-weight: 600; - padding: 2px 8px; - border-radius: 4px; + padding: 1px 6px; + border-radius: 3px; background: var(--colorbackinput); color: #fff; white-space: nowrap; @@ -1508,7 +1557,7 @@ body { .te-btn, .list-item, .contact-group-header, - .anlage-card { + .pwa-tree-row { min-height: 48px; } diff --git a/js/pwa.js b/js/pwa.js index 8f5f8df..10a0529 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -157,7 +157,7 @@ if (e.state && e.state.screen) { showScreen(e.state.screen, true); // Anlagen-Liste nachladen falls leer (z.B. nach Seiten-Refresh) - if (e.state.screen === 'anlagen' && App.customerId && !$('#anlagen-list').children('.anlage-card, .contact-group').length) { + if (e.state.screen === 'anlagen' && App.customerId && !$('#anlagen-list').children('.pwa-tree-node, .contact-list, .contact-group').length) { reloadAnlagen(); } } else { @@ -172,7 +172,7 @@ // Customer/Anlage selection $('#customer-list').on('click', '.list-item', handleCustomerSelect); - $('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect); + $('#anlagen-list').on('click', '.pwa-tree-row', handleTreeNodeClick); $('#anlagen-list').on('click', '.contact-group-header', handleContactGroupClick); // Editor actions @@ -469,14 +469,12 @@ html += ''; } - // Kunden-Anlagen (ohne Kontaktzuweisung) darunter + // Kunden-Anlagen (ohne Kontaktzuweisung) als Baum darunter if (anlagen && anlagen.length) { if (contacts && contacts.length && App.customerAddress) { html += `
${escapeHtml(App.customerName)} – ${escapeHtml(App.customerAddress)}
`; } - anlagen.forEach(a => { - html += renderAnlageCard(a); - }); + html += renderTreeNodes(anlagen, 0); } if (!html) { @@ -487,35 +485,105 @@ $('#anlagen-list').html(html); } - function renderAnlageCard(a) { - let fieldsHtml = ''; - if (a.fields && a.fields.length) { - fieldsHtml = '
'; - a.fields.forEach(f => { - const style = f.color ? ` style="background:${f.color}"` : ''; - fieldsHtml += `${escapeHtml(f.value)}`; - }); - fieldsHtml += '
'; - } - return ` -
-
- -
-
-
${escapeHtml(a.label || 'Anlage ' + a.id)}
- ${a.type ? '
' + escapeHtml(a.type) + '
' : ''} - ${fieldsHtml} -
- -
- `; + // Baum-Knoten rekursiv rendern + function renderTreeNodes(nodes, level) { + let html = ''; + nodes.forEach(a => { + const hasChildren = a.children && a.children.length > 0; + const isEquipment = a.can_have_equipment; + const isStructure = a.can_have_children && !isEquipment; + + // Typ-Klasse für farbliche Unterscheidung + let typeClass = 'node-leaf'; + if (isEquipment) typeClass = 'node-equipment'; + else if (isStructure) typeClass = 'node-structure'; + + // Feld-Badges + let fieldsHtml = ''; + if (a.fields && a.fields.length) { + fieldsHtml = '
'; + a.fields.forEach(f => { + const style = f.color ? ` style="background:${f.color}"` : ''; + fieldsHtml += `${escapeHtml(f.value)}`; + }); + fieldsHtml += '
'; + } + + // Icons je nach Typ + let iconSvg; + if (isEquipment) { + // Schaltschrank/Verteiler + iconSvg = ''; + } else if (isStructure) { + // Gebäude/Raum + iconSvg = ''; + } else { + // Endgerät + iconSvg = ''; + } + + html += `
`; + html += `
`; + + // Toggle-Chevron (nur bei Kindern) + if (hasChildren) { + html += ''; + } else { + html += ''; + } + + // Icon + html += `
${iconSvg}
`; + + // Inhalt + html += '
'; + html += `
${escapeHtml(a.label || 'Anlage ' + a.id)}
`; + if (a.type) html += `
${escapeHtml(a.type)}
`; + html += fieldsHtml; + html += '
'; + + // Editor-Pfeil nur bei Equipment-Containern + if (isEquipment) { + html += ''; + } + + html += '
'; // pwa-tree-row + + // Kinder (eingeklappt) + if (hasChildren) { + html += '
'; + html += renderTreeNodes(a.children, level + 1); + html += '
'; + } + + html += '
'; // pwa-tree-node + }); + return html; } - async function handleAnlageSelect() { - const id = $(this).data('id'); - const name = $(this).find('.anlage-card-title').text(); + // Baum-Knoten aufklappen/zuklappen + function handleTreeNodeClick(e) { + const $node = $(this).closest('.pwa-tree-node'); + // Bei Klick auf Editor-Pfeil → Editor öffnen + if ($(e.target).closest('.pwa-tree-open').length) { + openAnlageEditor($node.data('id'), $node.find('> .pwa-tree-row .pwa-tree-label').first().text()); + return; + } + + // Bei Equipment-Containern: Klick auf Content öffnet Editor + if ($node.hasClass('node-equipment') && !$(e.target).closest('.pwa-tree-toggle').length) { + openAnlageEditor($node.data('id'), $node.find('> .pwa-tree-row .pwa-tree-label').first().text()); + return; + } + + // Toggle Kinder + if ($node.hasClass('has-children')) { + $node.toggleClass('expanded'); + } + } + + async function openAnlageEditor(id, name) { App.anlageId = id; App.anlageName = name; $('#anlage-name').text(name); @@ -551,11 +619,7 @@ }); if (response.success && response.anlagen && response.anlagen.length) { - let html = ''; - response.anlagen.forEach(a => { - html += renderAnlageCard(a); - }); - $list.html(html); + $list.html(renderTreeNodes(response.anlagen, 0)); } else { $list.html('
Keine Anlagen
'); } diff --git a/pwa.php b/pwa.php index d5aa843..05ff0cc 100644 --- a/pwa.php +++ b/pwa.php @@ -44,7 +44,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db'); - + @@ -324,6 +324,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db'); window.DOLIBARR_URL = ''; window.MODULE_URL = '/custom/kundenkarte'; - + diff --git a/sw.js b/sw.js index 33ff85d..3bf9f44 100644 --- a/sw.js +++ b/sw.js @@ -3,8 +3,8 @@ * Offline-First für Schaltschrank-Dokumentation */ -const CACHE_NAME = 'kundenkarte-pwa-v2.7'; -const OFFLINE_CACHE = 'kundenkarte-offline-v2.7'; +const CACHE_NAME = 'kundenkarte-pwa-v2.8'; +const OFFLINE_CACHE = 'kundenkarte-offline-v2.8'; // Statische Assets die immer gecached werden const STATIC_ASSETS = [ diff --git a/tabs/anlagen.php b/tabs/anlagen.php index 3e3241c..fce9bda 100755 --- a/tabs/anlagen.php +++ b/tabs/anlagen.php @@ -365,7 +365,21 @@ if ($action == 'togglepin' && $permissiontoadd) { // Use Dolibarr standard button classes $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: URL-Parameter hat Vorrang, sonst Admin-Setting +$defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'); +$viewMode = GETPOST('view', 'aZ09'); +if (!in_array($viewMode, array('tree', 'graph'))) { + $viewMode = $defaultView; +} + +dol_include_once('/kundenkarte/lib/graph_view.lib.php'); + +$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time()); +$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time()); +kundenkarte_graph_add_assets($jsFiles, $cssFiles, $viewMode); + +llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles); // Prepare tabs $head = societe_prepare_head($object); @@ -442,38 +456,60 @@ if ($permissiontoadd) { // Get systems not yet enabled for this customer $availableSystems = array_diff_key($allSystems, $customerSystems); if (!empty($availableSystems)) { - print ''; } } print ''; -// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy) +// Steuerungs-Buttons (nur wenn kein Formular aktiv) $isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy')); if ($isTreeView) { - print '
'; - // Compact mode toggle (visible on mobile) - print ''; - print ''; - print ''; - if ($systemId > 0) { - $exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId; - print ''; - print ' PDF Export'; + $toggleView = ($viewMode === 'graph') ? 'tree' : 'graph'; + $toggleUrl = $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&view='.$toggleView; + $toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap'; + $toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView'); + + if ($viewMode !== 'graph') { + // Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper) + print '
'; + print ''; + print ' '.$toggleLabel; print ''; + print ''; + print ''; + print ''; + if ($systemId > 0) { + $exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId; + print ''; + print ' PDF Export'; + print ''; + } + print '
'; } - print '
'; } print ''; // End kundenkarte-system-tabs-wrapper +// Graph-Toolbar: UNTER der System-Tab-Borderlinie +if ($isTreeView && $viewMode === 'graph') { + kundenkarte_graph_print_toolbar(array( + 'socid' => $id, + 'contactid' => 0, + 'systemid' => $systemId, + 'viewMode' => $viewMode, + 'permissiontoadd' => $permissiontoadd, + 'pageUrl' => $_SERVER['PHP_SELF'], + )); +} + // Add system form (hidden by default) if ($permissiontoadd && !empty($availableSystems)) { print ''; } else { - // Tree view - if ($permissiontoadd) { + // Listenansicht (Baum oder Graph) + if ($permissiontoadd && $viewMode !== 'graph') { print '
'; print ''; print ' '.$langs->trans('AddElement'); @@ -1000,42 +1036,56 @@ if (empty($customerSystems)) { print '
'; } - // Load tree - $tree = $anlage->fetchTree($id, $systemId); - - // Pre-load all type fields for tooltip and tree display - $typeFieldsMap = array(); - $sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC"; - $resql = $db->query($sql); - if ($resql) { - while ($obj = $db->fetch_object($resql)) { - if (!isset($typeFieldsMap[$obj->fk_anlage_type])) { - $typeFieldsMap[$obj->fk_anlage_type] = array(); - } - $typeFieldsMap[$obj->fk_anlage_type][] = $obj; - } - $db->free($resql); - } - - // Pre-load all connections for this customer/system - dol_include_once('/kundenkarte/class/anlageconnection.class.php'); - $connObj = new AnlageConnection($db); - $allConnections = $connObj->fetchBySociete($id, $systemId); - // Index by target_id for quick lookup (connection shows ABOVE the target element) - $connectionsByTarget = array(); - foreach ($allConnections as $conn) { - if (!isset($connectionsByTarget[$conn->fk_target])) { - $connectionsByTarget[$conn->fk_target] = array(); - } - $connectionsByTarget[$conn->fk_target][] = $conn; - } - - if (!empty($tree)) { - print '
'; - printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget); - print '
'; + if ($viewMode === 'graph' && $isTreeView) { + // Graph-Ansicht: Container rendern, Daten werden per AJAX geladen + kundenkarte_graph_print_container(array( + 'socid' => $id, + 'contactid' => 0, + 'systemid' => $systemId, + 'permissiontoadd' => $permissiontoadd, + 'permissiontodelete' => $permissiontodelete, + 'pageUrl' => $_SERVER['PHP_SELF'], + )); } else { - print '
'.$langs->trans('NoInstallations').'
'; + // Baumansicht (klassisch) + + // Load tree + $tree = $anlage->fetchTree($id, $systemId); + + // Pre-load all type fields for tooltip and tree display + $typeFieldsMap = array(); + $sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC"; + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + if (!isset($typeFieldsMap[$obj->fk_anlage_type])) { + $typeFieldsMap[$obj->fk_anlage_type] = array(); + } + $typeFieldsMap[$obj->fk_anlage_type][] = $obj; + } + $db->free($resql); + } + + // Pre-load all connections for this customer/system + dol_include_once('/kundenkarte/class/anlageconnection.class.php'); + $connObj = new AnlageConnection($db); + $allConnections = $connObj->fetchBySociete($id, $systemId); + // Index by target_id for quick lookup (connection shows ABOVE the target element) + $connectionsByTarget = array(); + foreach ($allConnections as $conn) { + if (!isset($connectionsByTarget[$conn->fk_target])) { + $connectionsByTarget[$conn->fk_target] = array(); + } + $connectionsByTarget[$conn->fk_target][] = $conn; + } + + if (!empty($tree)) { + print '
'; + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget); + print '
'; + } else { + print '
'.$langs->trans('NoInstallations').'
'; + } } } } @@ -1160,11 +1210,18 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev } } - // CSS class based on whether node has its own cable connection + // CSS class basierend auf Kabel-Verbindung und Typ-Kategorie $nodeClass = 'kundenkarte-tree-node'; if (!$hasConnection && $level > 0) { $nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel } + if ($node->type_can_have_equipment) { + $nodeClass .= ' node-equipment'; // Geräte-Container (Schaltschrank, Verteiler) + } elseif ($node->type_can_have_children) { + $nodeClass .= ' node-structure'; // Gebäudeteil (Gebäude, Etage, Raum) + } else { + $nodeClass .= ' node-leaf'; // Endgerät + } print '
'; print '
'; @@ -1427,7 +1484,16 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete print ''; } - print '
'; + // Typ-Kategorie als CSS-Klasse + $nodeContentClass = 'kundenkarte-tree-node-content'; + if ($node->type_can_have_equipment) { + $nodeContentClass .= ' node-equipment'; + } elseif ($node->type_can_have_children) { + $nodeContentClass .= ' node-structure'; + } else { + $nodeContentClass .= ' node-leaf'; + } + print '
'; print '
'; if ($hasChildren) { diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php index af7827c..145f072 100755 --- a/tabs/contact_anlagen.php +++ b/tabs/contact_anlagen.php @@ -364,7 +364,20 @@ if ($action == 'togglepin' && $permissiontoadd) { */ $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: URL-Parameter hat Vorrang, sonst Admin-Setting +dol_include_once('/kundenkarte/lib/graph_view.lib.php'); +$defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'); +$viewMode = GETPOST('view', 'aZ09'); +if (!in_array($viewMode, array('tree', 'graph'))) { + $viewMode = $defaultView; +} + +$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time()); +$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time()); +kundenkarte_graph_add_assets($jsFiles, $cssFiles, $viewMode); + +llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles); // Prepare tabs $head = contact_prepare_head($object); @@ -441,38 +454,60 @@ if ($permissiontoadd) { // Get systems not yet enabled for this contact $availableSystems = array_diff_key($allSystems, $customerSystems); if (!empty($availableSystems)) { - print ''; } } print '
'; -// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy) +// Steuerungs-Buttons (nur wenn kein Formular aktiv) $isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy')); if ($isTreeView) { - print '
'; } print '
'; // End kundenkarte-system-tabs-wrapper +// Graph-Toolbar: UNTER der System-Tab-Borderlinie +if ($isTreeView && $viewMode === 'graph') { + kundenkarte_graph_print_toolbar(array( + 'socid' => $object->socid, + 'contactid' => $id, + 'systemid' => $systemId, + 'viewMode' => $viewMode, + 'permissiontoadd' => $permissiontoadd, + 'pageUrl' => $_SERVER['PHP_SELF'], + )); +} + // Add system form (hidden by default) if ($permissiontoadd && !empty($availableSystems)) { print ''; } else { - // Tree view - if ($permissiontoadd) { + // Listenansicht (Baum oder Graph) + if ($permissiontoadd && $viewMode !== 'graph') { print '
'; print ''; print ' '.$langs->trans('AddElement'); @@ -999,42 +1034,56 @@ if (empty($customerSystems)) { print '
'; } - // Load tree for this contact - $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); - - // Pre-load all type fields for tooltip and tree display - $typeFieldsMap = array(); - $sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC"; - $resql = $db->query($sql); - if ($resql) { - while ($obj = $db->fetch_object($resql)) { - if (!isset($typeFieldsMap[$obj->fk_anlage_type])) { - $typeFieldsMap[$obj->fk_anlage_type] = array(); - } - $typeFieldsMap[$obj->fk_anlage_type][] = $obj; - } - $db->free($resql); - } - - // Pre-load all connections for this contact/system - dol_include_once('/kundenkarte/class/anlageconnection.class.php'); - $connObj = new AnlageConnection($db); - $allConnections = $connObj->fetchBySociete($object->socid, $systemId); - // Index by target_id for quick lookup (connection shows ABOVE the target element) - $connectionsByTarget = array(); - foreach ($allConnections as $conn) { - if (!isset($connectionsByTarget[$conn->fk_target])) { - $connectionsByTarget[$conn->fk_target] = array(); - } - $connectionsByTarget[$conn->fk_target][] = $conn; - } - - if (!empty($tree)) { - print '
'; - printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget); - print '
'; + if ($viewMode === 'graph' && $isTreeView) { + // Graph-Ansicht: Container rendern, Daten werden per AJAX geladen + kundenkarte_graph_print_container(array( + 'socid' => $object->socid, + 'contactid' => $id, + 'systemid' => $systemId, + 'permissiontoadd' => $permissiontoadd, + 'permissiontodelete' => $permissiontodelete, + 'pageUrl' => $_SERVER['PHP_SELF'], + )); } else { - print '
'.$langs->trans('NoInstallations').'
'; + // Baumansicht (klassisch) + + // Load tree for this contact + $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); + + // Pre-load all type fields for tooltip and tree display + $typeFieldsMap = array(); + $sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC"; + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + if (!isset($typeFieldsMap[$obj->fk_anlage_type])) { + $typeFieldsMap[$obj->fk_anlage_type] = array(); + } + $typeFieldsMap[$obj->fk_anlage_type][] = $obj; + } + $db->free($resql); + } + + // Pre-load all connections for this contact/system + dol_include_once('/kundenkarte/class/anlageconnection.class.php'); + $connObj = new AnlageConnection($db); + $allConnections = $connObj->fetchBySociete($object->socid, $systemId); + // Index by target_id for quick lookup (connection shows ABOVE the target element) + $connectionsByTarget = array(); + foreach ($allConnections as $conn) { + if (!isset($connectionsByTarget[$conn->fk_target])) { + $connectionsByTarget[$conn->fk_target] = array(); + } + $connectionsByTarget[$conn->fk_target][] = $conn; + } + + if (!empty($tree)) { + print '
'; + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget); + print '
'; + } else { + print '
'.$langs->trans('NoInstallations').'
'; + } } } } @@ -1146,7 +1195,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $mainText = $conn->label ? $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 '
'; print ''; if ($mainText) { @@ -1159,11 +1208,18 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, } } - // CSS class based on whether node has its own cable connection + // CSS class basierend auf Kabel-Verbindung und Typ-Kategorie $nodeClass = 'kundenkarte-tree-node'; if (!$hasConnection && $level > 0) { $nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel } + if ($node->type_can_have_equipment) { + $nodeClass .= ' node-equipment'; // Geräte-Container (Schaltschrank, Verteiler) + } elseif ($node->type_can_have_children) { + $nodeClass .= ' node-structure'; // Gebäudeteil (Gebäude, Etage, Raum) + } else { + $nodeClass .= ' node-leaf'; // Endgerät + } print '
'; print '
'; @@ -1394,7 +1450,7 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe $mainText = $conn->label ? $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 '
'; // Draw vertical line columns (for cables passing through) @@ -1458,7 +1514,16 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe print ''; } - print '
'; + // Typ-Kategorie als CSS-Klasse + $nodeContentClass = 'kundenkarte-tree-node-content'; + if ($node->type_can_have_equipment) { + $nodeContentClass .= ' node-equipment'; + } elseif ($node->type_can_have_children) { + $nodeContentClass .= ' node-structure'; + } else { + $nodeContentClass .= ' node-leaf'; + } + print '
'; print '
'; if ($hasChildren) {