diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php
index 7dbe2d8..af7827c 100755
--- a/tabs/contact_anlagen.php
+++ b/tabs/contact_anlagen.php
@@ -26,6 +26,9 @@ require_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php';
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/anlagetype.class.php');
dol_include_once('/kundenkarte/class/anlagefile.class.php');
+dol_include_once('/kundenkarte/class/equipmentpanel.class.php');
+dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
+dol_include_once('/kundenkarte/class/equipment.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Load translation files
@@ -63,7 +66,11 @@ if ($id <= 0) {
$id = $tmpAnlage->fk_contact;
if ($id > 0) {
// Redirect to include id in URL for proper navigation
- header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action='.$action.'&anlage_id='.$anlageId);
+ $redirectUrl = $_SERVER['PHP_SELF'].'?id='.$id;
+ if ($systemId > 0) $redirectUrl .= '&system='.$systemId;
+ if ($action) $redirectUrl .= '&action='.urlencode($action);
+ if ($anlageId > 0) $redirectUrl .= '&anlage_id='.$anlageId;
+ header('Location: '.$redirectUrl);
exit;
}
}
@@ -171,12 +178,6 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
- $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml');
- $anlage->model = GETPOST('model', 'alphanohtml');
- $anlage->serial_number = GETPOST('serial_number', 'alphanohtml');
- $anlage->power_rating = GETPOST('power_rating', 'alphanohtml');
- $anlage->location = GETPOST('location', 'alphanohtml');
- $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear'));
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
@@ -190,10 +191,11 @@ if ($action == 'add' && $permissiontoadd) {
}
}
- // Dynamic fields
+ // All fields come from dynamic fields now
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
+ if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
@@ -214,15 +216,10 @@ if ($action == 'add' && $permissiontoadd) {
if ($action == 'update' && $permissiontoadd) {
$anlage->fetch($anlageId);
+ $systemIdBefore = $anlage->fk_system; // Remember original system
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
- $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml');
- $anlage->model = GETPOST('model', 'alphanohtml');
- $anlage->serial_number = GETPOST('serial_number', 'alphanohtml');
- $anlage->power_rating = GETPOST('power_rating', 'alphanohtml');
- $anlage->location = GETPOST('location', 'alphanohtml');
- $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear'));
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type - but keep current system for GLOBAL types (buildings)
@@ -235,10 +232,11 @@ if ($action == 'update' && $permissiontoadd) {
}
}
- // Dynamic fields
+ // All fields come from dynamic fields now
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
+ if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
@@ -270,7 +268,7 @@ if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
exit;
}
-// File upload
+// File upload (multi-file support)
if ($action == 'uploadfile' && $permissiontoadd) {
$anlage->fetch($anlageId);
$upload_dir = $anlage->getFileDirectory();
@@ -280,23 +278,47 @@ if ($action == 'uploadfile' && $permissiontoadd) {
dol_mkdir($upload_dir);
}
- if (!empty($_FILES['userfile']['name'])) {
- $result = dol_add_file_process($upload_dir, 0, 1, 'userfile', '', null, '', 1);
- if ($result > 0) {
- // Add to database
- $anlagefile = new AnlageFile($db);
- $anlagefile->fk_anlage = $anlageId;
- $anlagefile->filename = dol_sanitizeFileName($_FILES['userfile']['name']);
- // IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path!
- $anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$anlagefile->filename;
- $anlagefile->filesize = $_FILES['userfile']['size'];
- $anlagefile->mimetype = dol_mimetype($anlagefile->filename);
- $anlagefile->create($user);
+ $uploadCount = 0;
+ if (!empty($_FILES['userfiles']['name'][0])) {
+ // Multi-file upload
+ $fileCount = count($_FILES['userfiles']['name']);
+ for ($i = 0; $i < $fileCount; $i++) {
+ if ($_FILES['userfiles']['error'][$i] === UPLOAD_ERR_OK && !empty($_FILES['userfiles']['name'][$i])) {
+ $tmpName = $_FILES['userfiles']['tmp_name'][$i];
+ $fileName = dol_sanitizeFileName($_FILES['userfiles']['name'][$i]);
+ $destPath = $upload_dir.'/'.$fileName;
- // Generate thumbnail
- $anlagefile->generateThumbnail();
+ // Handle duplicate filenames
+ $counter = 1;
+ $baseName = pathinfo($fileName, PATHINFO_FILENAME);
+ $extension = pathinfo($fileName, PATHINFO_EXTENSION);
+ while (file_exists($destPath)) {
+ $fileName = $baseName.'_'.$counter.'.'.$extension;
+ $destPath = $upload_dir.'/'.$fileName;
+ $counter++;
+ }
- setEventMessages($langs->trans('FileUploaded'), null, 'mesgs');
+ if (move_uploaded_file($tmpName, $destPath)) {
+ // Add to database
+ $anlagefile = new AnlageFile($db);
+ $anlagefile->fk_anlage = $anlageId;
+ $anlagefile->filename = $fileName;
+ // IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path!
+ $anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$fileName;
+ $anlagefile->filesize = $_FILES['userfiles']['size'][$i];
+ $anlagefile->mimetype = dol_mimetype($fileName);
+ $anlagefile->create($user);
+
+ // Generate thumbnail
+ $anlagefile->generateThumbnail();
+
+ $uploadCount++;
+ }
+ }
+ }
+ if ($uploadCount > 0) {
+ $msg = $uploadCount > 1 ? $uploadCount.' Dateien hochgeladen' : 'Datei hochgeladen';
+ setEventMessages($msg, null, 'mesgs');
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId);
@@ -321,14 +343,28 @@ if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete)
exit;
}
+// Toggle file pinned status
+if ($action == 'togglepin' && $permissiontoadd) {
+ $fileId = GETPOSTINT('fileid');
+ if ($fileId > 0) {
+ $anlagefile = new AnlageFile($db);
+ if ($anlagefile->fetch($fileId) > 0) {
+ if ($anlagefile->togglePin($user) > 0) {
+ $msg = $anlagefile->is_pinned ? $langs->trans('FilePinned') : $langs->trans('FileUnpinned');
+ setEventMessages($msg, null, 'mesgs');
+ }
+ }
+ }
+ header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId.'#files');
+ exit;
+}
+
/*
* View
*/
-// Use Dolibarr standard button classes
-
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs);
-llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/kundenkarte.js?v=1769963241'), array('/kundenkarte/css/kundenkarte.css?v=1769964233'));
+llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/pathfinding.min.js', '/kundenkarte/js/kundenkarte.js?v='.time()), array('/kundenkarte/css/kundenkarte.css?v='.time()));
// Prepare tabs
$head = contact_prepare_head($object);
@@ -416,6 +452,10 @@ print '';
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
print '
';
+ // Compact mode toggle (visible on mobile)
+ print '
';
print '
';
@@ -488,38 +528,26 @@ if (empty($customerSystems)) {
print '
| '.$langs->trans('Type').' | ';
print ''.dol_escape_htmltag($anlage->type_label).' |
';
- if ($anlage->manufacturer) {
- print '
| '.$langs->trans('FieldManufacturer').' | ';
- print ''.dol_escape_htmltag($anlage->manufacturer).' |
';
- }
- if ($anlage->model) {
- print '
| '.$langs->trans('FieldModel').' | ';
- print ''.dol_escape_htmltag($anlage->model).' |
';
- }
- if ($anlage->serial_number) {
- print '
| '.$langs->trans('FieldSerialNumber').' | ';
- print ''.dol_escape_htmltag($anlage->serial_number).' |
';
- }
- if ($anlage->power_rating) {
- print '
| '.$langs->trans('FieldPowerRating').' | ';
- print ''.dol_escape_htmltag($anlage->power_rating).' |
';
- }
- if ($anlage->location) {
- print '
| '.$langs->trans('FieldLocation').' | ';
- print ''.dol_escape_htmltag($anlage->location).' |
';
- }
- if ($anlage->installation_date) {
- print '
| '.$langs->trans('FieldInstallationDate').' | ';
- print ''.dol_print_date($anlage->installation_date, 'day').' |
';
- }
-
- // Dynamic fields
+ // Dynamic fields - all fields come from type definition
$fieldValues = $anlage->getFieldValues();
- foreach ($type->fields as $field) {
- if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') {
- print '
| '.dol_escape_htmltag($field->field_label).' | ';
- $value = $fieldValues[$field->field_code];
- print ''.dol_escape_htmltag($value).' |
';
+ $typeFieldsList = $type->fetchFields();
+ foreach ($typeFieldsList as $field) {
+ if ($field->field_type === 'header') {
+ // Section header
+ print '
| '.dol_escape_htmltag($field->field_label).' |
|---|
';
+ } else {
+ $value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
+ if ($value !== '') {
+ print '
| '.dol_escape_htmltag($field->field_label).' | ';
+ // Format date fields
+ if ($field->field_type === 'date' && $value) {
+ print ''.dol_print_date(strtotime($value), 'day').' |
';
+ } elseif ($field->field_type === 'checkbox') {
+ print '
'.($value ? $langs->trans('Yes') : $langs->trans('No')).' | ';
+ } else {
+ print '
'.dol_escape_htmltag($value).' | ';
+ }
+ }
}
}
@@ -549,17 +577,29 @@ if (empty($customerSystems)) {
print '
'.$langs->trans('AttachedFiles').'
';
if ($permissiontoadd) {
- print '
';
}
if (!empty($files)) {
print '
';
foreach ($files as $file) {
- print '
';
+ $pinnedClass = $file->is_pinned ? ' kundenkarte-file-pinned' : '';
+ print '
';
+ if ($file->is_pinned) {
+ print '
';
+ }
print '
';
if ($file->file_type == 'image') {
$thumbUrl = $file->getThumbUrl();
@@ -569,19 +609,46 @@ if (empty($customerSystems)) {
print '
.')
';
}
} elseif ($file->file_type == 'pdf') {
- // PDF preview using iframe - 50% smaller, no toolbar
+ // PDF preview using iframe
print '
';
print '';
print '
';
} else {
- print '
';
+ // Show icon based on file type
+ $fileIcon = 'fa-file-o';
+ $iconColor = '#999';
+ if ($file->file_type == 'archive') {
+ $fileIcon = 'fa-file-archive-o';
+ $iconColor = '#f39c12';
+ } elseif ($file->file_type == 'document') {
+ $ext = strtolower(pathinfo($file->filename, PATHINFO_EXTENSION));
+ if (in_array($ext, array('doc', 'docx'))) {
+ $fileIcon = 'fa-file-word-o';
+ $iconColor = '#2b579a';
+ } elseif (in_array($ext, array('xls', 'xlsx'))) {
+ $fileIcon = 'fa-file-excel-o';
+ $iconColor = '#217346';
+ } elseif ($ext == 'txt') {
+ $fileIcon = 'fa-file-text-o';
+ $iconColor = '#666';
+ } else {
+ $fileIcon = 'fa-file-text-o';
+ $iconColor = '#3498db';
+ }
+ }
+ print '
';
}
print '
';
print '
';
- print '
'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'
';
+ print '
'.dol_escape_htmltag(dol_trunc($file->filename, 35)).'
';
print '
'.dol_print_size($file->filesize).'
';
print '
';
print '
';
+ if ($permissiontoadd) {
+ $pinClass = $file->is_pinned ? ' kundenkarte-file-btn-pinned' : '';
+ $pinTitle = $file->is_pinned ? $langs->trans('Unpin') : $langs->trans('Pin');
+ print '
';
+ }
if ($permissiontodelete) {
print '
';
}
@@ -594,6 +661,70 @@ if (empty($customerSystems)) {
print '
'.$langs->trans('NoFiles').'
';
}
+ // Equipment section (only if type can have equipment)
+ if ($type->can_have_equipment) {
+ print '
'.$langs->trans('Equipment').' - Schaltplan
';
+
+ // Equipment container
+ print '
';
+
+ // Schematic Editor
+ print '
';
+ print '';
+ print '
';
+ print '
';
+
+ print '
'; // .kundenkarte-equipment-container
+
+ // Initialize SchematicEditor JavaScript
+ print '';
+ }
+
// Action buttons
print '
';
if ($permissiontoadd) {
@@ -885,9 +1016,22 @@ if (empty($customerSystems)) {
$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);
+ print '
';
+ printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '
';
} else {
print '
'.$langs->trans('NoInstallations').'
';
@@ -906,14 +1050,15 @@ llxFooter();
$db->close();
/**
- * Print tree recursively
+ * Print tree recursively (root level - handles cable line assignment)
*/
-function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array())
+function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
+ $fieldValues = $node->getFieldValues();
- // Build tooltip data for hover on icon (only dynamic fields now)
+ // Build tooltip data - only label, type and dynamic fields
$tooltipData = array(
'label' => $node->label,
'type' => $node->type_label,
@@ -921,10 +1066,11 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
'fields' => array()
);
- // Collect dynamic fields for tooltip (show_in_hover) and tree label (show_in_tree)
- $treeInfoParts = array();
+ // Collect fields for tooltip (show_in_hover) and tree label (show_in_tree)
+ $treeInfoBadges = array(); // Fields to display as badges (right side)
+ $treeInfoParentheses = array(); // Fields to display in parentheses (after label)
+
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
- $fieldValues = $node->getFieldValues();
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
// Handle header fields
if ($fieldDef->field_type === 'header') {
@@ -961,17 +1107,64 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day');
}
- $treeInfoParts[] = array(
+ // Store as array with field info
+ $fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
- 'type' => $fieldDef->field_type
+ 'type' => $fieldDef->field_type,
+ 'color' => $fieldDef->badge_color ?? ''
);
+ // Sort into badge or parentheses based on field setting
+ $displayMode = $fieldDef->tree_display_mode ?? 'badge';
+ if ($displayMode === 'parentheses') {
+ $treeInfoParentheses[] = $fieldInfo;
+ } else {
+ $treeInfoBadges[] = $fieldInfo;
+ }
}
}
}
- print '
';
+ // Show cable connections TO this node (BEFORE the element - cable appears above verbraucher)
+ $hasConnection = !empty($connectionsByTarget[$node->id]);
+ if ($hasConnection) {
+ foreach ($connectionsByTarget[$node->id] as $conn) {
+ // Build cable info (type + spec + length)
+ $cableInfo = '';
+ if ($conn->medium_type_label || $conn->medium_type_text) {
+ $cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
+ }
+ if ($conn->medium_spec) {
+ $cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
+ }
+ if ($conn->medium_length) {
+ $cableInfo .= ' ('.$conn->medium_length.')';
+ }
+
+ // If label exists, show cable info as badge. Otherwise show cable info as main text
+ $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;
+ print '
';
+ print '';
+ if ($mainText) {
+ print ''.dol_escape_htmltag($mainText).'';
+ }
+ if ($badgeText) {
+ print ' '.dol_escape_htmltag($badgeText).'';
+ }
+ print '';
+ }
+ }
+
+ // CSS class based on whether node has its own cable connection
+ $nodeClass = 'kundenkarte-tree-node';
+ if (!$hasConnection && $level > 0) {
+ $nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel
+ }
+ print '
';
print '
';
// Toggle
@@ -985,48 +1178,47 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '
'.kundenkarte_render_icon($picto).'';
- // Label with tree info badges + file indicators
+ // Label with parentheses info (directly after label, only values)
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
- print '
'.dol_escape_htmltag($node->label).'';
+ print '
'.dol_escape_htmltag($node->label);
- // Tree info display based on settings
- $treeInfoDisplay = getDolGlobalString('KUNDENKARTE_TREE_INFO_DISPLAY', 'badge');
- $badgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
-
- if (!empty($treeInfoParts) && $treeInfoDisplay !== 'none') {
- if ($treeInfoDisplay === 'badge') {
- // Display as badges with icons
- print '';
- foreach ($treeInfoParts as $info) {
- $badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
- print '';
- print ' '.dol_escape_htmltag($info['value']);
- print '';
- }
- print '';
- } else {
- // Display in parentheses (old style)
- print ' (';
- $infoTexts = array();
- foreach ($treeInfoParts as $info) {
- $infoTexts[] = dol_escape_htmltag($info['label']).': '.dol_escape_htmltag($info['value']);
- }
- print implode(', ', $infoTexts);
- print ')';
+ // Parentheses info directly after label (only values, no field names)
+ if (!empty($treeInfoParentheses)) {
+ $infoValues = array();
+ foreach ($treeInfoParentheses as $info) {
+ $infoValues[] = dol_escape_htmltag($info['value']);
}
+ print ' ('.implode(', ', $infoValues).')';
+ }
+ print '';
+
+ // Spacer to push badges to the right
+ print '
';
+
+ // Badges (far right, before actions)
+ $defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
+ if (!empty($treeInfoBadges)) {
+ print '
';
+ foreach ($treeInfoBadges as $info) {
+ $badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
+ // Use field-specific color if set, otherwise global default
+ $fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
+ print '';
+ print ' '.dol_escape_htmltag($info['value']);
+ print '';
+ }
+ print '';
}
// File indicators
if ($node->image_count > 0 || $node->doc_count > 0) {
$totalFiles = $node->image_count + $node->doc_count;
- print '
';
- // Combined file badge with file icon
+ print '';
print '';
print ' '.$totalFiles;
print '';
print '';
}
- print '';
// Type badge
if ($node->type_short || $node->type_label) {
@@ -1039,6 +1231,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '
';
if ($canEdit) {
print '
';
+ print '
';
print '
';
print '
';
}
@@ -1049,10 +1242,72 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '
';
- // Children
+ // Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) {
- print '
';
- printTree($node->children, $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap);
+ // First pass: assign cable index to each child with cable
+ $cableCount = 0;
+ $childCableIndex = array(); // child_id => cable_index
+ foreach ($node->children as $child) {
+ if (!empty($connectionsByTarget[$child->id])) {
+ $cableCount++;
+ $childCableIndex[$child->id] = $cableCount;
+ }
+ }
+
+ print '
';
+
+ // Render children - each row has cable line columns on the left
+ $prevHadCable = false;
+ $isFirst = true;
+ foreach ($node->children as $child) {
+ $myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
+ $hasOwnCable = !empty($connectionsByTarget[$child->id]);
+
+ // Add spacer row before elements with their own cable (except first)
+ if ($hasOwnCable && !$isFirst) {
+ // Spacer row with active cable lines passing through
+ $spacerActiveLines = array();
+ $foundCurrent = false;
+ foreach ($node->children as $c) {
+ if ($c->id == $child->id) {
+ $foundCurrent = true;
+ }
+ if ($foundCurrent && isset($childCableIndex[$c->id])) {
+ $spacerActiveLines[] = $childCableIndex[$c->id];
+ }
+ }
+
+ print '
';
+ for ($i = $cableCount; $i >= 1; $i--) {
+ $isActive = in_array($i, $spacerActiveLines);
+ $lineClass = 'cable-line';
+ if ($isActive) {
+ $lineClass .= ' active';
+ }
+ print '
';
+ }
+ print '
';
+ print '
';
+ }
+
+ // Find which cable lines are still active (run past this row to children below)
+ $childActiveLinesAfter = array();
+ $foundCurrent = false;
+ foreach ($node->children as $c) {
+ if ($c->id == $child->id) {
+ $foundCurrent = true;
+ continue;
+ }
+ if ($foundCurrent && isset($childCableIndex[$c->id])) {
+ $childActiveLinesAfter[] = $childCableIndex[$c->id];
+ }
+ }
+
+ printTreeWithCableLines(array($child), $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $cableCount, $childActiveLinesAfter);
+
+ $prevHadCable = $hasOwnCable;
+ $isFirst = false;
+ }
print '
';
}
@@ -1060,6 +1315,292 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
}
}
+/**
+ * Print tree with multiple parallel cable lines
+ * Each child with its own cable gets a vertical line that runs from top past all siblings
+ */
+function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array(), $myCableIndex = 0, $totalCables = 0, $activeLinesAfter = array())
+{
+ foreach ($nodes as $node) {
+ $hasChildren = !empty($node->children);
+ $hasConnection = !empty($connectionsByTarget[$node->id]);
+ $fieldValues = $node->getFieldValues();
+
+ // Build tooltip data
+ $tooltipData = array(
+ 'label' => $node->label,
+ 'type' => $node->type_label,
+ 'note_html' => $node->note_private ? nl2br(htmlspecialchars($node->note_private, ENT_QUOTES, 'UTF-8')) : '',
+ 'fields' => array()
+ );
+
+ $treeInfoBadges = array();
+ $treeInfoParentheses = array();
+ if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
+ foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
+ if ($fieldDef->field_type === 'header') continue;
+ $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
+ if ($fieldDef->show_in_hover && $value !== '') {
+ $displayValue = $value;
+ if ($fieldDef->field_type === 'date' && $value) {
+ $displayValue = dol_print_date(strtotime($value), 'day');
+ }
+ $tooltipData['fields'][$fieldDef->field_code] = array(
+ 'label' => $fieldDef->field_label,
+ 'value' => $displayValue,
+ 'type' => $fieldDef->field_type
+ );
+ }
+ if ($fieldDef->show_in_tree && $value !== '') {
+ $displayVal = ($fieldDef->field_type === 'date' && $value) ? dol_print_date(strtotime($value), 'day') : $value;
+ $fieldInfo = array(
+ 'label' => $fieldDef->field_label,
+ 'value' => $displayVal,
+ 'code' => $fieldDef->field_code,
+ 'type' => $fieldDef->field_type,
+ 'color' => $fieldDef->badge_color ?? ''
+ );
+ $displayMode = $fieldDef->tree_display_mode ?? 'badge';
+ if ($displayMode === 'parentheses') {
+ $treeInfoParentheses[] = $fieldInfo;
+ } else {
+ $treeInfoBadges[] = $fieldInfo;
+ }
+ }
+ }
+ }
+
+ // All lines that need to be drawn for this row (active lines passing through + my line if I have cable)
+ $allLines = $activeLinesAfter;
+ if ($hasConnection && $myCableIndex > 0) {
+ $allLines[] = $myCableIndex;
+ }
+ sort($allLines);
+
+ // Cable connection row (BEFORE the element)
+ if ($hasConnection) {
+ foreach ($connectionsByTarget[$node->id] as $conn) {
+ $cableInfo = '';
+ if ($conn->medium_type_label || $conn->medium_type_text) {
+ $cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
+ }
+ if ($conn->medium_spec) {
+ $cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
+ }
+ if ($conn->medium_length) {
+ $cableInfo .= ' ('.$conn->medium_length.')';
+ }
+
+ $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;
+ print '
';
+
+ // Draw vertical line columns (for cables passing through)
+ // Reverse order: index 1 (first element) = rightmost, highest index = leftmost
+ for ($i = $totalCables; $i >= 1; $i--) {
+ $isActive = in_array($i, $activeLinesAfter);
+ $isMyLine = ($i == $myCableIndex);
+ $lineClass = 'cable-line';
+ $inlineStyle = '';
+ if ($isActive) {
+ $lineClass .= ' active'; // Line continues down
+ }
+ if ($isMyLine) {
+ $lineClass .= ' my-line conn-line'; // This is my cable line - ends here with horizontal
+ // Calculate width: columns to the right * 15px + 8px to reach element border
+ $columnsToRight = $i - 1;
+ $horizontalWidth = ($columnsToRight * 15) + 8;
+ $inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
+ }
+ print '
';
+ }
+
+ print '
';
+ print '';
+ if ($mainText) {
+ print ''.dol_escape_htmltag($mainText).'';
+ }
+ if ($badgeText) {
+ print ' '.dol_escape_htmltag($badgeText).'';
+ }
+ print '';
+ print '
';
+ }
+ }
+
+ // Node row
+ $nodeClass = 'kundenkarte-tree-row node-row';
+ if (!$hasConnection && $level > 0) {
+ $nodeClass .= ' no-cable';
+ }
+
+ print '
';
+
+ // Draw vertical line columns (for cables passing through)
+ // Reverse order: index 1 (first element) = rightmost, highest index = leftmost
+ for ($i = $totalCables; $i >= 1; $i--) {
+ $isActive = in_array($i, $activeLinesAfter);
+ $isMyLine = ($i == $myCableIndex && $hasConnection);
+ $lineClass = 'cable-line';
+ $inlineStyle = '';
+ if ($isActive) {
+ $lineClass .= ' active';
+ }
+ if ($isMyLine) {
+ $lineClass .= ' my-line node-line'; // Horizontal connector to this node
+ // Calculate width: columns to the right * 15px + 8px to reach element border
+ $columnsToRight = $i - 1;
+ $horizontalWidth = ($columnsToRight * 15) + 8;
+ $inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
+ }
+ print '
';
+ }
+
+ print '
';
+ print '
';
+
+ if ($hasChildren) {
+ print '
';
+ } else {
+ print '
';
+ }
+
+ $picto = $node->type_picto ? $node->type_picto : 'fa-cube';
+ print '
'.kundenkarte_render_icon($picto).'';
+
+ $viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
+ // Label with parentheses info (only values, no field names)
+ print '
'.dol_escape_htmltag($node->label);
+ if (!empty($treeInfoParentheses)) {
+ $infoValues = array();
+ foreach ($treeInfoParentheses as $info) {
+ $infoValues[] = dol_escape_htmltag($info['value']);
+ }
+ print ' ('.implode(', ', $infoValues).')';
+ }
+ print '';
+
+ // Spacer to push badges to the right
+ print '
';
+
+ // Badges (far right)
+ if (!empty($treeInfoBadges)) {
+ $defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
+ print '
';
+ foreach ($treeInfoBadges as $info) {
+ $badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
+ $fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
+ print '';
+ print ' '.dol_escape_htmltag($info['value']);
+ print '';
+ }
+ print '';
+ }
+
+ // File indicators
+ if ($node->image_count > 0 || $node->doc_count > 0) {
+ $totalFiles = $node->image_count + $node->doc_count;
+ print '
';
+ print '';
+ print ' '.$totalFiles;
+ print '';
+ print '';
+ }
+
+ if ($node->type_short || $node->type_label) {
+ $typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
+ print '
'.dol_escape_htmltag($typeDisplay).'';
+ }
+
+ print '
';
+ print '';
+ if ($canEdit) {
+ print '';
+ print '';
+ print '';
+ print '';
+ }
+ if ($canDelete) {
+ print '';
+ }
+ print '';
+ print '
';
+ print '
'; // node-content
+
+ print '
'; // tree-row
+
+ // Children
+ if ($hasChildren) {
+ // First pass: assign cable index to each child with cable
+ $childCableCount = 0;
+ $childCableIndex = array(); // child_id => cable_index
+ foreach ($node->children as $child) {
+ if (!empty($connectionsByTarget[$child->id])) {
+ $childCableCount++;
+ $childCableIndex[$child->id] = $childCableCount;
+ }
+ }
+
+ print '
';
+
+ $prevHadCable = false;
+ $isFirst = true;
+ foreach ($node->children as $child) {
+ $myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
+ $hasOwnCable = !empty($connectionsByTarget[$child->id]);
+
+ // Add spacer row before elements with their own cable (except first)
+ if ($hasOwnCable && !$isFirst) {
+ // Spacer row with active cable lines passing through
+ $spacerActiveLines = array();
+ $foundCurrent = false;
+ foreach ($node->children as $c) {
+ if ($c->id == $child->id) {
+ $foundCurrent = true;
+ }
+ if ($foundCurrent && isset($childCableIndex[$c->id])) {
+ $spacerActiveLines[] = $childCableIndex[$c->id];
+ }
+ }
+
+ print '
';
+ for ($i = $childCableCount; $i >= 1; $i--) {
+ $isActive = in_array($i, $spacerActiveLines);
+ $lineClass = 'cable-line';
+ if ($isActive) {
+ $lineClass .= ' active';
+ }
+ print '
';
+ }
+ print '
';
+ print '
';
+ }
+
+ // Find which cable lines are still active (run past this row to children below)
+ $childActiveLinesAfter = array();
+ $foundCurrent = false;
+ foreach ($node->children as $c) {
+ if ($c->id == $child->id) {
+ $foundCurrent = true;
+ continue;
+ }
+ if ($foundCurrent && isset($childCableIndex[$c->id])) {
+ $childActiveLinesAfter[] = $childCableIndex[$c->id];
+ }
+ }
+
+ printTreeWithCableLines(array($child), $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $childCableCount, $childActiveLinesAfter);
+
+ $prevHadCable = $hasOwnCable;
+ $isFirst = false;
+ }
+ print '
';
+ }
+ }
+}
+
/**
* Print tree options for select
*/