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 '
'; + print ''; print ''; - print ''; - print ' '; + print '
'; + print ''; + print '
'; + print ''; + print '

Dateien hierher ziehen oder durchsuchen

'; + print '

Bilder, PDF, Office-Dokumente, ZIP-Archive

'; + print '
'; + print ''; + print '
'; + print ''; 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 ''.$langs->trans('SchematicEditor').' (Klick auf Block = Bearbeiten | Drag = Verschieben | + = Hinzufügen)'; + print '
'; + print '
'; + // Zoom controls + print '
'; + print ''; + print '100%'; + print ''; + print ''; + print ''; + print '
'; + // Manual wire draw toggle + print ''; + print ''; + print ''; + // BOM (Stückliste) button + print ''; + // Audit Log button + print ''; + // PDF Export button + $pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L'; + print ''; + print ' PDF Export'; + print ''; + print '
'; + print '
'; + 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 */