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 += `