Kontakt-Anlagen auf Stand von Kunden-Anlagen gebracht

- Baum: tree_display_mode + badge_color pro Feld, Spacer-Layout
- Kabelverbindungen: printTreeWithCableLines() mit parallelen Linien
- Schaltplan-Editor (Equipment) komplett integriert
- Datei-Upload: Drag&Drop Dropzone, Multi-File, Pin/Unpin
- Datei-Icons: differenziert (Word, Excel, Archive, Text)
- View: Header-Support, Datums-/Checkbox-Formatierung
- Actions: alte statische Felder entfernt, Header-Skip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-21 21:10:31 +01:00
parent 45469df529
commit 0a151270ec

View file

@ -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/anlage.class.php');
dol_include_once('/kundenkarte/class/anlagetype.class.php'); dol_include_once('/kundenkarte/class/anlagetype.class.php');
dol_include_once('/kundenkarte/class/anlagefile.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'); dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Load translation files // Load translation files
@ -63,7 +66,11 @@ if ($id <= 0) {
$id = $tmpAnlage->fk_contact; $id = $tmpAnlage->fk_contact;
if ($id > 0) { if ($id > 0) {
// Redirect to include id in URL for proper navigation // 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; exit;
} }
} }
@ -171,12 +178,6 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent'); $anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId; $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->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1; $anlage->status = 1;
@ -190,10 +191,11 @@ if ($action == 'add' && $permissiontoadd) {
} }
} }
// Dynamic fields // All fields come from dynamic fields now
$fieldValues = array(); $fieldValues = array();
$fields = $type->fetchFields(); $fields = $type->fetchFields();
foreach ($fields as $field) { foreach ($fields as $field) {
if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml'); $value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') { if ($value !== '') {
$fieldValues[$field->field_code] = $value; $fieldValues[$field->field_code] = $value;
@ -214,15 +216,10 @@ if ($action == 'add' && $permissiontoadd) {
if ($action == 'update' && $permissiontoadd) { if ($action == 'update' && $permissiontoadd) {
$anlage->fetch($anlageId); $anlage->fetch($anlageId);
$systemIdBefore = $anlage->fk_system; // Remember original system
$anlage->label = GETPOST('label', 'alphanohtml'); $anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent'); $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'] : ''; $anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type - but keep current system for GLOBAL types (buildings) // 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(); $fieldValues = array();
$fields = $type->fetchFields(); $fields = $type->fetchFields();
foreach ($fields as $field) { foreach ($fields as $field) {
if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml'); $value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') { if ($value !== '') {
$fieldValues[$field->field_code] = $value; $fieldValues[$field->field_code] = $value;
@ -270,7 +268,7 @@ if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
exit; exit;
} }
// File upload // File upload (multi-file support)
if ($action == 'uploadfile' && $permissiontoadd) { if ($action == 'uploadfile' && $permissiontoadd) {
$anlage->fetch($anlageId); $anlage->fetch($anlageId);
$upload_dir = $anlage->getFileDirectory(); $upload_dir = $anlage->getFileDirectory();
@ -280,23 +278,47 @@ if ($action == 'uploadfile' && $permissiontoadd) {
dol_mkdir($upload_dir); dol_mkdir($upload_dir);
} }
if (!empty($_FILES['userfile']['name'])) { $uploadCount = 0;
$result = dol_add_file_process($upload_dir, 0, 1, 'userfile', '', null, '', 1); if (!empty($_FILES['userfiles']['name'][0])) {
if ($result > 0) { // Multi-file upload
// Add to database $fileCount = count($_FILES['userfiles']['name']);
$anlagefile = new AnlageFile($db); for ($i = 0; $i < $fileCount; $i++) {
$anlagefile->fk_anlage = $anlageId; if ($_FILES['userfiles']['error'][$i] === UPLOAD_ERR_OK && !empty($_FILES['userfiles']['name'][$i])) {
$anlagefile->filename = dol_sanitizeFileName($_FILES['userfile']['name']); $tmpName = $_FILES['userfiles']['tmp_name'][$i];
// IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path! $fileName = dol_sanitizeFileName($_FILES['userfiles']['name'][$i]);
$anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$anlagefile->filename; $destPath = $upload_dir.'/'.$fileName;
$anlagefile->filesize = $_FILES['userfile']['size'];
$anlagefile->mimetype = dol_mimetype($anlagefile->filename);
$anlagefile->create($user);
// Generate thumbnail // Handle duplicate filenames
$anlagefile->generateThumbnail(); $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); 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; 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 * View
*/ */
// Use Dolibarr standard button classes
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs); $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 // Prepare tabs
$head = contact_prepare_head($object); $head = contact_prepare_head($object);
@ -416,6 +452,10 @@ print '</div>';
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy')); $isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) { if ($isTreeView) {
print '<div class="kundenkarte-tree-controls">'; 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 '<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 '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>'; print '</button>';
@ -488,38 +528,26 @@ if (empty($customerSystems)) {
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>'; print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>'; print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
if ($anlage->manufacturer) { // Dynamic fields - all fields come from type definition
print '<tr><td>'.$langs->trans('FieldManufacturer').'</td>';
print '<td>'.dol_escape_htmltag($anlage->manufacturer).'</td></tr>';
}
if ($anlage->model) {
print '<tr><td>'.$langs->trans('FieldModel').'</td>';
print '<td>'.dol_escape_htmltag($anlage->model).'</td></tr>';
}
if ($anlage->serial_number) {
print '<tr><td>'.$langs->trans('FieldSerialNumber').'</td>';
print '<td>'.dol_escape_htmltag($anlage->serial_number).'</td></tr>';
}
if ($anlage->power_rating) {
print '<tr><td>'.$langs->trans('FieldPowerRating').'</td>';
print '<td>'.dol_escape_htmltag($anlage->power_rating).'</td></tr>';
}
if ($anlage->location) {
print '<tr><td>'.$langs->trans('FieldLocation').'</td>';
print '<td>'.dol_escape_htmltag($anlage->location).'</td></tr>';
}
if ($anlage->installation_date) {
print '<tr><td>'.$langs->trans('FieldInstallationDate').'</td>';
print '<td>'.dol_print_date($anlage->installation_date, 'day').'</td></tr>';
}
// Dynamic fields
$fieldValues = $anlage->getFieldValues(); $fieldValues = $anlage->getFieldValues();
foreach ($type->fields as $field) { $typeFieldsList = $type->fetchFields();
if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') { foreach ($typeFieldsList as $field) {
print '<tr><td>'.dol_escape_htmltag($field->field_label).'</td>'; if ($field->field_type === 'header') {
$value = $fieldValues[$field->field_code]; // Section header
print '<td>'.dol_escape_htmltag($value).'</td></tr>'; print '<tr class="liste_titre"><th colspan="2" style="background:#f0f0f0;padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
} else {
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
if ($value !== '') {
print '<tr><td>'.dol_escape_htmltag($field->field_label).'</td>';
// Format date fields
if ($field->field_type === 'date' && $value) {
print '<td>'.dol_print_date(strtotime($value), 'day').'</td></tr>';
} elseif ($field->field_type === 'checkbox') {
print '<td>'.($value ? $langs->trans('Yes') : $langs->trans('No')).'</td></tr>';
} else {
print '<td>'.dol_escape_htmltag($value).'</td></tr>';
}
}
} }
} }
@ -549,17 +577,29 @@ if (empty($customerSystems)) {
print '<br><h4>'.$langs->trans('AttachedFiles').'</h4>'; print '<br><h4>'.$langs->trans('AttachedFiles').'</h4>';
if ($permissiontoadd) { if ($permissiontoadd) {
print '<form method="POST" enctype="multipart/form-data" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=uploadfile&anlage_id='.$anlageId.'">'; print '<form method="POST" enctype="multipart/form-data" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=uploadfile&anlage_id='.$anlageId.'" id="fileUploadForm">';
print '<input type="hidden" name="token" value="'.newToken().'">'; print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="file" name="userfile" accept="image/*,.pdf,.doc,.docx">'; print '<div class="kundenkarte-dropzone" id="fileDropzone">';
print ' <button type="submit" class="button">'.$langs->trans('Upload').'</button>'; print '<input type="file" name="userfiles[]" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.odt,.ods,.txt,.rtf,.zip,.rar,.7z,.tar,.gz" style="display:none;">';
print '<div class="kundenkarte-dropzone-content">';
print '<i class="fa fa-cloud-upload" style="font-size:32px;color:#3498db;margin-bottom:10px;"></i>';
print '<p style="margin:0;color:#888;">Dateien hierher ziehen oder <a href="#" onclick="document.getElementById(\'fileInput\').click();return false;" style="color:#3498db;">durchsuchen</a></p>';
print '<p style="margin:5px 0 0;font-size:11px;color:#666;">Bilder, PDF, Office-Dokumente, ZIP-Archive</p>';
print '</div>';
print '<div class="kundenkarte-dropzone-files" id="selectedFiles" style="display:none;"></div>';
print '</div>';
print '<button type="submit" class="button" id="uploadBtn" style="display:none;margin-top:10px;">'.$langs->trans('Upload').' (<span id="fileCount">0</span> Dateien)</button>';
print '</form><br>'; print '</form><br>';
} }
if (!empty($files)) { if (!empty($files)) {
print '<div class="kundenkarte-files-grid">'; print '<div class="kundenkarte-files-grid">';
foreach ($files as $file) { foreach ($files as $file) {
print '<div class="kundenkarte-file-item">'; $pinnedClass = $file->is_pinned ? ' kundenkarte-file-pinned' : '';
print '<div class="kundenkarte-file-item'.$pinnedClass.'">';
if ($file->is_pinned) {
print '<div class="kundenkarte-pin-indicator" title="'.$langs->trans('Pinned').'"><i class="fa fa-thumb-tack"></i></div>';
}
print '<div class="kundenkarte-file-preview">'; print '<div class="kundenkarte-file-preview">';
if ($file->file_type == 'image') { if ($file->file_type == 'image') {
$thumbUrl = $file->getThumbUrl(); $thumbUrl = $file->getThumbUrl();
@ -569,19 +609,46 @@ if (empty($customerSystems)) {
print '<img src="'.$file->getUrl().'" alt="" style="max-width:100%;max-height:100%;">'; print '<img src="'.$file->getUrl().'" alt="" style="max-width:100%;max-height:100%;">';
} }
} elseif ($file->file_type == 'pdf') { } elseif ($file->file_type == 'pdf') {
// PDF preview using iframe - 50% smaller, no toolbar // PDF preview using iframe
print '<div class="kundenkarte-pdf-preview-wrapper">'; print '<div class="kundenkarte-pdf-preview-wrapper">';
print '<iframe src="'.$file->getUrl().'#page=1&toolbar=0&navpanes=0&statusbar=0&view=FitH" class="kundenkarte-pdf-preview-frame"></iframe>'; print '<iframe src="'.$file->getUrl().'#page=1&toolbar=0&navpanes=0&statusbar=0&view=FitH" class="kundenkarte-pdf-preview-frame"></iframe>';
print '</div>'; print '</div>';
} else { } else {
print '<i class="fa fa-file-o" style="font-size:48px;color:#999;"></i>'; // 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 '<i class="fa '.$fileIcon.'" style="font-size:48px;color:'.$iconColor.';"></i>';
} }
print '</div>'; print '</div>';
print '<div class="kundenkarte-file-info">'; print '<div class="kundenkarte-file-info">';
print '<div class="kundenkarte-file-name" title="'.dol_escape_htmltag($file->filename).'">'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'</div>'; print '<div class="kundenkarte-file-name" title="'.dol_escape_htmltag($file->filename).'">'.dol_escape_htmltag(dol_trunc($file->filename, 35)).'</div>';
print '<div class="kundenkarte-file-size">'.dol_print_size($file->filesize).'</div>'; print '<div class="kundenkarte-file-size">'.dol_print_size($file->filesize).'</div>';
print '<div class="kundenkarte-file-actions">'; print '<div class="kundenkarte-file-actions">';
print '<a href="'.$file->getUrl().'" target="_blank" class="kundenkarte-file-btn" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$file->getUrl().'" target="_blank" class="kundenkarte-file-btn" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($permissiontoadd) {
$pinClass = $file->is_pinned ? ' kundenkarte-file-btn-pinned' : '';
$pinTitle = $file->is_pinned ? $langs->trans('Unpin') : $langs->trans('Pin');
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=togglepin&anlage_id='.$anlageId.'&fileid='.$file->id.'" class="kundenkarte-file-btn'.$pinClass.'" title="'.$pinTitle.'"><i class="fa fa-thumb-tack"></i></a>';
}
if ($permissiontodelete) { if ($permissiontodelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=askdeletefile&anlage_id='.$anlageId.'&fileid='.$file->id.'" class="kundenkarte-file-btn kundenkarte-file-btn-delete" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=askdeletefile&anlage_id='.$anlageId.'&fileid='.$file->id.'" class="kundenkarte-file-btn kundenkarte-file-btn-delete" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a>';
} }
@ -594,6 +661,70 @@ if (empty($customerSystems)) {
print '<p class="opacitymedium">'.$langs->trans('NoFiles').'</p>'; print '<p class="opacitymedium">'.$langs->trans('NoFiles').'</p>';
} }
// Equipment section (only if type can have equipment)
if ($type->can_have_equipment) {
print '<br><h4><i class="fa fa-microchip"></i> '.$langs->trans('Equipment').' - Schaltplan</h4>';
// Equipment container
print '<div class="kundenkarte-equipment-container" data-anlage-id="'.$anlageId.'" data-system-id="'.$systemId.'">';
// Schematic Editor
print '<div class="schematic-editor-wrapper">';
print '<div class="schematic-editor-header" style="display:flex;justify-content:space-between;align-items:center;padding:10px 15px;background:#252525;border:1px solid #333;border-radius:4px 4px 0 0;">';
print '<div style="color:#3498db;">';
print '<strong>'.$langs->trans('SchematicEditor').'</strong> <span style="color:#888;font-size:0.85em;">(Klick auf Block = Bearbeiten | Drag = Verschieben | + = Hinzufügen)</span>';
print '</div>';
print '<div class="schematic-editor-actions" style="display:flex;gap:10px;align-items:center;">';
// Zoom controls
print '<div class="schematic-zoom-controls" style="display:flex;gap:2px;align-items:center;background:#222;border-radius:3px;padding:2px;">';
print '<button type="button" class="schematic-zoom-out" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#fff;cursor:pointer;" title="Verkleinern (Ctrl+Scroll)"><i class="fa fa-minus"></i></button>';
print '<span class="schematic-zoom-level" style="min-width:45px;text-align:center;color:#888;font-size:12px;">100%</span>';
print '<button type="button" class="schematic-zoom-in" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#fff;cursor:pointer;" title="Vergrößern (Ctrl+Scroll)"><i class="fa fa-plus"></i></button>';
print '<button type="button" class="schematic-zoom-fit" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;margin-left:3px;" title="Einpassen"><i class="fa fa-compress"></i></button>';
print '<button type="button" class="schematic-zoom-reset" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#888;cursor:pointer;" title="100%"><i class="fa fa-search"></i></button>';
print '</div>';
// Manual wire draw toggle
print '<button type="button" class="schematic-wire-draw-toggle" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#27ae60;cursor:pointer;" title="Manueller Zeichenmodus: Leitungen selbst zeichnen mit Raster-Snap">';
print '<i class="fa fa-pencil"></i> Manuell zeichnen';
print '</button>';
print '<button type="button" class="schematic-add-busbar" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#f39c12;cursor:pointer;" title="Phasenschiene hinzufügen">';
print '<i class="fa fa-arrows-h"></i> Phasenschiene';
print '</button>';
print '<button type="button" class="schematic-clear-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#e74c3c;cursor:pointer;">';
print '<i class="fa fa-trash"></i> Alle Verbindungen löschen';
print '</button>';
// BOM (Stückliste) button
print '<button type="button" class="schematic-bom-generate" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#9b59b6;cursor:pointer;" title="Stückliste (Material) aus Schaltplan generieren">';
print '<i class="fa fa-list-alt"></i> Stückliste';
print '</button>';
// Audit Log button
print '<button type="button" class="schematic-audit-log" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#95a5a6;cursor:pointer;" title="Änderungsprotokoll anzeigen">';
print '<i class="fa fa-history"></i> Protokoll';
print '</button>';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '<a href="'.$pdfExportUrl.'" target="_blank" class="schematic-export-pdf" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="PDF Export (Leitungslaufplan nach DIN EN 61082)">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
print '</div>';
print '</div>';
print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">';
print '<div class="schematic-message" style="display:none;padding:8px 15px;margin-bottom:10px;border-radius:4px;font-size:12px;"></div>';
print '</div>';
print '</div>';
print '</div>'; // .kundenkarte-equipment-container
// Initialize SchematicEditor JavaScript
print '<script>
$(document).ready(function() {
if (typeof KundenKarte !== "undefined" && KundenKarte.SchematicEditor) {
KundenKarte.SchematicEditor.init('.$anlageId.');
}
});
</script>';
}
// Action buttons // Action buttons
print '<div class="tabsAction">'; print '<div class="tabsAction">';
if ($permissiontoadd) { if ($permissiontoadd) {
@ -885,9 +1016,22 @@ if (empty($customerSystems)) {
$db->free($resql); $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)) { if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'">'; print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap); printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>'; print '</div>';
} else { } else {
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>'; print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
@ -906,14 +1050,15 @@ llxFooter();
$db->close(); $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) { foreach ($nodes as $node) {
$hasChildren = !empty($node->children); $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( $tooltipData = array(
'label' => $node->label, 'label' => $node->label,
'type' => $node->type_label, 'type' => $node->type_label,
@ -921,10 +1066,11 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
'fields' => array() 'fields' => array()
); );
// Collect dynamic fields for tooltip (show_in_hover) and tree label (show_in_tree) // Collect fields for tooltip (show_in_hover) and tree label (show_in_tree)
$treeInfoParts = array(); $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])) { if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
$fieldValues = $node->getFieldValues();
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) { foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
// Handle header fields // Handle header fields
if ($fieldDef->field_type === 'header') { if ($fieldDef->field_type === 'header') {
@ -961,17 +1107,64 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
if ($fieldDef->field_type === 'date' && $value) { if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day'); $displayVal = dol_print_date(strtotime($value), 'day');
} }
$treeInfoParts[] = array( // Store as array with field info
$fieldInfo = array(
'label' => $fieldDef->field_label, 'label' => $fieldDef->field_label,
'value' => $displayVal, 'value' => $displayVal,
'code' => $fieldDef->field_code, '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 '<div class="kundenkarte-tree-node" style="margin-left:'.($level * 20).'px;">'; // 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 '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn">';
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
if ($mainText) {
print '<span class="conn-main">'.dol_escape_htmltag($mainText).'</span>';
}
if ($badgeText) {
print ' <span class="conn-label">'.dol_escape_htmltag($badgeText).'</span>';
}
print '</a>';
}
}
// 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 '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">'; print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
// Toggle // Toggle
@ -985,48 +1178,47 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
$picto = $node->type_picto ? $node->type_picto : 'fa-cube'; $picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>'; print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
// 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; $viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label).'</span>'; print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
// Tree info display based on settings // Parentheses info directly after label (only values, no field names)
$treeInfoDisplay = getDolGlobalString('KUNDENKARTE_TREE_INFO_DISPLAY', 'badge'); if (!empty($treeInfoParentheses)) {
$badgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e'); $infoValues = array();
foreach ($treeInfoParentheses as $info) {
if (!empty($treeInfoParts) && $treeInfoDisplay !== 'none') { $infoValues[] = dol_escape_htmltag($info['value']);
if ($treeInfoDisplay === 'badge') {
// Display as badges with icons
print '<span class="kundenkarte-tree-badges">';
foreach ($treeInfoParts as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$badgeColor.' 0%, '.kundenkarte_adjust_color($badgeColor, -20).' 100%);">';
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
print '</span>';
}
print '</span>';
} else {
// Display in parentheses (old style)
print '<span class="kundenkarte-tree-label-info"> (';
$infoTexts = array();
foreach ($treeInfoParts as $info) {
$infoTexts[] = dol_escape_htmltag($info['label']).': '.dol_escape_htmltag($info['value']);
}
print implode(', ', $infoTexts);
print ')</span>';
} }
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
}
print '</span>';
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
// Badges (far right, before actions)
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
if (!empty($treeInfoBadges)) {
print '<span class="kundenkarte-tree-badges">';
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 '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
print '</span>';
}
print '</span>';
} }
// File indicators // File indicators
if ($node->image_count > 0 || $node->doc_count > 0) { if ($node->image_count > 0 || $node->doc_count > 0) {
$totalFiles = $node->image_count + $node->doc_count; $totalFiles = $node->image_count + $node->doc_count;
print ' <span class="kundenkarte-tree-files">'; print '<span class="kundenkarte-tree-files">';
// Combined file badge with file icon
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">'; print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">';
print '<i class="fa fa-paperclip"></i> '.$totalFiles; print '<i class="fa fa-paperclip"></i> '.$totalFiles;
print '</a>'; print '</a>';
print '</span>'; print '</span>';
} }
print '</span>';
// Type badge // Type badge
if ($node->type_short || $node->type_label) { if ($node->type_short || $node->type_label) {
@ -1039,6 +1231,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) { if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$node->fk_soc.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>'; print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
} }
@ -1049,10 +1242,72 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '</div>'; print '</div>';
// Children // Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) { if ($hasChildren) {
print '<div class="kundenkarte-tree-children">'; // First pass: assign cable index to each child with cable
printTree($node->children, $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap); $cableCount = 0;
$childCableIndex = array(); // child_id => cable_index
foreach ($node->children as $child) {
if (!empty($connectionsByTarget[$child->id])) {
$cableCount++;
$childCableIndex[$child->id] = $cableCount;
}
}
print '<div class="kundenkarte-tree-children" data-cable-count="'.$cableCount.'">';
// 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 '<div class="kundenkarte-tree-row spacer-row">';
for ($i = $cableCount; $i >= 1; $i--) {
$isActive = in_array($i, $spacerActiveLines);
$lineClass = 'cable-line';
if ($isActive) {
$lineClass .= ' active';
}
print '<span class="'.$lineClass.'" data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content"></div>';
print '</div>';
}
// 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 '</div>'; print '</div>';
} }
@ -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 '<div class="kundenkarte-tree-row">';
// 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 '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn-content">';
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
if ($mainText) {
print '<span class="conn-main">'.dol_escape_htmltag($mainText).'</span>';
}
if ($badgeText) {
print ' <span class="conn-label">'.dol_escape_htmltag($badgeText).'</span>';
}
print '</a>';
print '</div>';
}
}
// Node row
$nodeClass = 'kundenkarte-tree-row node-row';
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable';
}
print '<div class="'.$nodeClass.'">';
// 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 '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
if ($hasChildren) {
print '<span class="kundenkarte-tree-toggle"><i class="fa fa-chevron-down"></i></span>';
} else {
print '<span class="kundenkarte-tree-toggle" style="visibility:hidden;"><i class="fa fa-chevron-down"></i></span>';
}
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
// Label with parentheses info (only values, no field names)
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
if (!empty($treeInfoParentheses)) {
$infoValues = array();
foreach ($treeInfoParentheses as $info) {
$infoValues[] = dol_escape_htmltag($info['value']);
}
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
}
print '</span>';
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
// Badges (far right)
if (!empty($treeInfoBadges)) {
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
print '<span class="kundenkarte-tree-badges">';
foreach ($treeInfoBadges as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
print '</span>';
}
print '</span>';
}
// File indicators
if ($node->image_count > 0 || $node->doc_count > 0) {
$totalFiles = $node->image_count + $node->doc_count;
print '<span class="kundenkarte-tree-files">';
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">';
print '<i class="fa fa-paperclip"></i> '.$totalFiles;
print '</a>';
print '</span>';
}
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '<span class="kundenkarte-tree-type badge badge-secondary">'.dol_escape_htmltag($typeDisplay).'</span>';
}
print '<span class="kundenkarte-tree-actions">';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$node->fk_soc.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
}
print '</span>';
print '</div>';
print '</div>'; // node-content
print '</div>'; // 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 '<div class="kundenkarte-tree-children" data-cable-count="'.$childCableCount.'">';
$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 '<div class="kundenkarte-tree-row spacer-row">';
for ($i = $childCableCount; $i >= 1; $i--) {
$isActive = in_array($i, $spacerActiveLines);
$lineClass = 'cable-line';
if ($isActive) {
$lineClass .= ' active';
}
print '<span class="'.$lineClass.'" data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content"></div>';
print '</div>';
}
// 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 '</div>';
}
}
}
/** /**
* Print tree options for select * Print tree options for select
*/ */