feat: Graph-Toggle, Baum-Farben, PWA-Baumansicht

- Graph-Integration von feature/cytoscape-graph auf main portiert
  (anlagen.php + contact_anlagen.php: View-Mode, Toolbar, Container)
- Baum-Knoten farblich unterschieden: grün=Gebäude, blau=Equipment, orange=Endgerät
  (CSS border-left + Icon-Farbe je nach can_have_children/can_have_equipment)
- PWA: Kompletter Anlagen-Baum statt flache Liste
  (API liefert rekursiven Baum, Frontend mit aufklappbaren Knoten)
- PWA: Equipment-Container öffnen Editor, Strukturknoten klappen auf/zu
- Connection-URLs in contact_anlagen.php: contactid Parameter ergänzt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-26 12:09:13 +01:00
parent da4ed40ad2
commit 143ddcb958
9 changed files with 511 additions and 233 deletions

View file

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

View file

@ -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,";

View file

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

View file

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

138
js/pwa.js
View file

@ -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 += '</div>';
}
// Kunden-Anlagen (ohne Kontaktzuweisung) darunter
// Kunden-Anlagen (ohne Kontaktzuweisung) als Baum darunter
if (anlagen && anlagen.length) {
if (contacts && contacts.length && App.customerAddress) {
html += `<div class="anlagen-section-label">${escapeHtml(App.customerName)} ${escapeHtml(App.customerAddress)}</div>`;
}
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 = '<div class="anlage-card-fields">';
a.fields.forEach(f => {
const style = f.color ? ` style="background:${f.color}"` : '';
fieldsHtml += `<span class="anlage-field-badge"${style}>${escapeHtml(f.value)}</span>`;
});
fieldsHtml += '</div>';
}
return `
<div class="anlage-card" data-id="${a.id}">
<div class="anlage-card-icon">
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>
</div>
<div class="anlage-card-content">
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
${a.type ? '<div class="anlage-card-type">' + escapeHtml(a.type) + '</div>' : ''}
${fieldsHtml}
</div>
<svg class="anlage-card-arrow" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</div>
`;
// 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 = '<div class="anlage-card-fields">';
a.fields.forEach(f => {
const style = f.color ? ` style="background:${f.color}"` : '';
fieldsHtml += `<span class="anlage-field-badge"${style}>${escapeHtml(f.value)}</span>`;
});
fieldsHtml += '</div>';
}
// Icons je nach Typ
let iconSvg;
if (isEquipment) {
// Schaltschrank/Verteiler
iconSvg = '<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>';
} else if (isStructure) {
// Gebäude/Raum
iconSvg = '<svg viewBox="0 0 24 24"><path d="M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3zm0 2.84L18 12v7h-2v-6H8v6H6v-7l6-6.16z"/></svg>';
} else {
// Endgerät
iconSvg = '<svg viewBox="0 0 24 24"><path d="M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z"/></svg>';
}
html += `<div class="pwa-tree-node ${typeClass}${hasChildren ? ' has-children' : ''}" data-id="${a.id}" data-level="${level}">`;
html += `<div class="pwa-tree-row" style="padding-left:${12 + level * 20}px">`;
// Toggle-Chevron (nur bei Kindern)
if (hasChildren) {
html += '<svg class="pwa-tree-toggle" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
} else {
html += '<span class="pwa-tree-toggle-spacer"></span>';
}
// Icon
html += `<div class="pwa-tree-icon ${typeClass}">${iconSvg}</div>`;
// Inhalt
html += '<div class="pwa-tree-content">';
html += `<div class="pwa-tree-label">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>`;
if (a.type) html += `<div class="pwa-tree-type">${escapeHtml(a.type)}</div>`;
html += fieldsHtml;
html += '</div>';
// Editor-Pfeil nur bei Equipment-Containern
if (isEquipment) {
html += '<svg class="pwa-tree-open" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
}
html += '</div>'; // pwa-tree-row
// Kinder (eingeklappt)
if (hasChildren) {
html += '<div class="pwa-tree-children">';
html += renderTreeNodes(a.children, level + 1);
html += '</div>';
}
html += '</div>'; // 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('<div class="list-empty small">Keine Anlagen</div>');
}

View file

@ -44,7 +44,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="img/pwa-icon-192.png">
<link rel="apple-touch-icon" href="img/pwa-icon-192.png">
<link rel="stylesheet" href="css/pwa.css?v=2.7">
<link rel="stylesheet" href="css/pwa.css?v=2.8">
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
</head>
<body>
@ -324,6 +324,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
</script>
<script src="js/pwa.js?v=2.7"></script>
<script src="js/pwa.js?v=2.8"></script>
</body>
</html>

4
sw.js
View file

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

View file

@ -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 '<button type="button" class="button small" style="margin-left:auto;" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
print '<button type="button" class="button small kundenkarte-add-system-btn" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
print '</button>';
}
}
print '</div>';
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
// Steuerungs-Buttons (nur wenn kein Formular aktiv)
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
print '<div class="kundenkarte-tree-controls">';
// Compact mode toggle (visible on mobile)
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
print '<i class="fa fa-file-pdf-o"></i> 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 '<div class="kundenkarte-tree-controls">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel;
print '</a>';
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
}
print '</div>';
}
print '</div>';
}
print '</div>'; // 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 '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
@ -991,8 +1027,8 @@ if (empty($customerSystems)) {
print '</div>';
} else {
// Tree view
if ($permissiontoadd) {
// Listenansicht (Baum oder Graph)
if ($permissiontoadd && $viewMode !== 'graph') {
print '<div style="margin-bottom:15px;">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
@ -1000,42 +1036,56 @@ if (empty($customerSystems)) {
print '</div>';
}
// 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 '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
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 '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
// 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 '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
} else {
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
}
}
}
}
@ -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 '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
@ -1427,7 +1484,16 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
print '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content">';
// 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 '<div class="'.$nodeContentClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
if ($hasChildren) {

View file

@ -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 '<button type="button" class="button small" style="margin-left:auto;" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
print '<button type="button" class="button small kundenkarte-add-system-btn" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
print '</button>';
}
}
print '</div>';
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
// Steuerungs-Buttons (nur wenn kein Formular aktiv)
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
print '<div class="kundenkarte-tree-controls">';
// Compact mode toggle (visible on mobile)
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$object->socid.'&contactid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
print '<i class="fa fa-file-pdf-o"></i> 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 '<div class="kundenkarte-tree-controls">';
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel;
print '</a>';
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$object->socid.'&contactid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
}
print '</div>';
}
print '</div>';
}
print '</div>'; // End kundenkarte-system-tabs-wrapper
// Graph-Toolbar: UNTER der System-Tab-Borderlinie
if ($isTreeView && $viewMode === 'graph') {
kundenkarte_graph_print_toolbar(array(
'socid' => $object->socid,
'contactid' => $id,
'systemid' => $systemId,
'viewMode' => $viewMode,
'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'],
));
}
// Add system form (hidden by default)
if ($permissiontoadd && !empty($availableSystems)) {
print '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
@ -990,8 +1025,8 @@ if (empty($customerSystems)) {
print '</div>';
} else {
// Tree view
if ($permissiontoadd) {
// Listenansicht (Baum oder Graph)
if ($permissiontoadd && $viewMode !== 'graph') {
print '<div style="margin-bottom:15px;">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
@ -999,42 +1034,56 @@ if (empty($customerSystems)) {
print '</div>';
}
// 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 '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
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 '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
// 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 '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
} else {
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
}
}
}
}
@ -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 '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn">';
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
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 '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
@ -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 '<div class="kundenkarte-tree-row">';
// Draw vertical line columns (for cables passing through)
@ -1458,7 +1514,16 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
print '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content">';
// 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 '<div class="'.$nodeContentClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
if ($hasChildren) {