';
}
// Add system button (always on the right)
if ($permissiontoadd) {
// Get systems not yet enabled for this customer
$availableSystems = array_diff_key($allSystems, $customerSystems);
if (!empty($availableSystems)) {
print '';
}
}
print '
';
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
if ($viewMode === 'graph') {
// Graph Controls: Aktionen + Zoom
print '
';
if ($user->hasRight('kundenkarte', 'write')) {
print '
';
} elseif ($systemId > 0) {
// Show form or tree for selected system
if (in_array($action, array('create', 'edit', 'view', 'copy'))) {
// Load element for edit/view/copy
if ($action != 'create' && $anlageId > 0) {
$anlage->fetch($anlageId);
$type = new AnlageType($db);
$type->fetch($anlage->fk_anlage_type);
$type->fetchFields();
}
// Load types for select
$types = $anlageType->fetchAllBySystem($systemId);
print '
';
if ($action == 'view') {
// View mode
print '
'.dol_escape_htmltag($anlage->label).'
';
print '
';
print '
'.$langs->trans('Type').'
';
print '
'.dol_escape_htmltag($anlage->type_label).'
';
// Dynamic fields - all fields come from type definition
$fieldValues = $anlage->getFieldValues();
$typeFieldsList = $type->fetchFields();
foreach ($typeFieldsList as $field) {
if ($field->field_type === 'header') {
// Section header
print '
';
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
print '';
print '
';
} else {
// 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 '
';
print dol_get_fiche_end();
// Tooltip container
print '';
llxFooter();
$db->close();
/**
* Print tree recursively
*/
function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$fieldValues = $node->getFieldValues();
// Build tooltip data - only label, type and dynamic fields
$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()
);
// 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])) {
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
// Handle header fields
if ($fieldDef->field_type === 'header') {
if ($fieldDef->show_in_hover) {
$tooltipData['fields'][$fieldDef->field_code] = array(
'label' => $fieldDef->field_label,
'value' => '',
'type' => 'header'
);
}
continue;
}
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
// Add to tooltip if show_in_hover
if ($fieldDef->show_in_hover && $value !== '') {
// Format date values
$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
);
}
// Add to tree label info if show_in_tree
if ($fieldDef->show_in_tree && $value !== '') {
// Format date for tree info too
$displayVal = $value;
if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day');
}
// Store as array with field info
$fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
'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;
}
}
}
}
// 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='.$socid.'&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
if ($hasChildren) {
print '';
} else {
print '';
}
// Icon with tooltip data
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print ''.kundenkarte_render_icon($picto).'';
// Label with parentheses info (directly after label, only values)
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
print ''.dol_escape_htmltag($node->label);
// 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 '';
print '';
print ' '.$totalFiles;
print '';
print '';
}
// Type badge
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print ''.dol_escape_htmltag($typeDisplay).'';
}
// Actions
print '';
print '';
if ($canEdit) {
print '';
print '';
print '';
print '';
}
if ($canDelete) {
print '';
}
print '';
print '
';
// Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) {
// 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
foreach ($node->children as $child) {
$myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
// Find which cable lines are still active (run past this row to children below)
// These are cables from children that appear AFTER this one in the list
$activeLinesAfter = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
continue;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$activeLinesAfter[] = $childCableIndex[$c->id];
}
}
printTreeWithCableLines(array($child), $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $cableCount, $activeLinesAfter);
}
print '
';
}
print '
';
}
}
/**
* 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, $socid, $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='.$socid.'&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 '
'; // 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 '
';
// Find the last equipment and next free position for quick-duplicate button
$lastEquipment = null;
$nextFreePos = 1;
if (!empty($carrier->equipment)) {
$lastEquipment = end($carrier->equipment);
// Find the position right after the last equipment
$nextFreePos = $lastEquipment->position_te + $lastEquipment->width_te;
}
for ($pos = 1; $pos <= $carrier->total_te; $pos++) {
if (!isset($occupiedSlots[$pos])) {
$slotLeft = $PADDING_LEFT + ($pos - 1) * $TE_WIDTH;
// Check if this is the "quick duplicate" position (first free slot after last equipment)
if ($lastEquipment && $pos == $nextFreePos && ($pos + $lastEquipment->width_te - 1) <= $carrier->total_te) {
// Quick duplicate button - copies the last equipment
$html .= '