loadLangs(array('companies', 'kundenkarte@kundenkarte')); // Get parameters $id = GETPOSTINT('id'); $action = GETPOST('action', 'aZ09'); $confirm = GETPOST('confirm', 'alpha'); $systemId = GETPOSTINT('system'); $anlageId = GETPOSTINT('anlage_id'); $parentId = GETPOSTINT('parent_id'); // Security check if (!$user->hasRight('kundenkarte', 'read')) { accessforbidden(); } // Initialize objects $object = new Societe($db); $form = new Form($db); $anlage = new Anlage($db); $anlageType = new AnlageType($db); // Load thirdparty if ($id > 0) { $result = $object->fetch($id); if ($result <= 0) { dol_print_error($db, $object->error); exit; } } $permissiontoread = $user->hasRight('kundenkarte', 'read'); $permissiontoadd = $user->hasRight('kundenkarte', 'write'); $permissiontodelete = $user->hasRight('kundenkarte', 'delete'); // Load ALL available systems (from dictionary) $allSystems = array(); $sql = "SELECT rowid, code, label, picto, color FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { $allSystems[$obj->rowid] = $obj; } } // Load systems ENABLED for this customer (only thirdparty-level, not contact-specific) $customerSystems = array(); $sql = "SELECT ss.rowid, ss.fk_system, s.code, s.label, s.picto, s.color FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system ss JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system s ON s.rowid = ss.fk_system WHERE ss.fk_soc = ".((int) $id)." AND (ss.fk_contact IS NULL OR ss.fk_contact = 0) AND ss.active = 1 AND s.active = 1 ORDER BY s.position ASC"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { $customerSystems[$obj->fk_system] = $obj; } } // Default to first enabled system if not specified if (empty($systemId) && !empty($customerSystems)) { $systemId = array_key_first($customerSystems); } /* * Actions */ // Add system to customer if ($action == 'add_system' && $permissiontoadd) { $newSystemId = GETPOSTINT('new_system_id'); if ($newSystemId > 0 && !isset($customerSystems[$newSystemId])) { $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system"; $sql .= " (entity, fk_soc, fk_system, date_creation, fk_user_creat, active)"; $sql .= " VALUES (".$conf->entity.", ".((int) $id).", ".((int) $newSystemId).", NOW(), ".((int) $user->id).", 1)"; $result = $db->query($sql); if ($result) { setEventMessages($langs->trans('SystemAdded'), null, 'mesgs'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$newSystemId); exit; } else { setEventMessages($db->lasterror(), null, 'errors'); } } $action = ''; } // Remove system from customer if ($action == 'confirm_remove_system' && $confirm == 'yes' && $permissiontodelete) { $removeSystemId = GETPOSTINT('remove_system_id'); if ($removeSystemId > 0) { // Check if system has any elements (only thirdparty-level, not contact-specific) $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_soc = ".((int) $id)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId); $resql = $db->query($sql); $obj = $db->fetch_object($resql); if ($obj->cnt > 0) { setEventMessages($langs->trans('ErrorSystemHasElements'), null, 'errors'); } else { $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system WHERE fk_soc = ".((int) $id)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId); $db->query($sql); setEventMessages($langs->trans('SystemRemoved'), null, 'mesgs'); // Switch to another system or none unset($customerSystems[$removeSystemId]); if (!empty($customerSystems)) { $systemId = array_key_first($customerSystems); } else { $systemId = 0; } } } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.($systemId ? '&system='.$systemId : '')); exit; } if ($action == 'add' && $permissiontoadd) { $anlage->label = GETPOST('label', 'alphanohtml'); $anlage->fk_soc = $id; $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); $anlage->fk_parent = GETPOSTINT('fk_parent'); $anlage->fk_system = $systemId; $anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : ''; $anlage->status = 1; // Get type for system ID $type = new AnlageType($db); if ($type->fetch($anlage->fk_anlage_type) > 0) { $anlage->fk_system = $type->fk_system; } // 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; } } $anlage->setFieldValues($fieldValues); $result = $anlage->create($user); if ($result > 0) { setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); exit; } else { setEventMessages($anlage->error, $anlage->errors, 'errors'); $action = 'create'; } } if ($action == 'update' && $permissiontoadd) { $anlage->fetch($anlageId); $anlage->label = GETPOST('label', 'alphanohtml'); $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); $anlage->fk_parent = GETPOSTINT('fk_parent'); $anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : ''; // Get type for system ID $type = new AnlageType($db); if ($type->fetch($anlage->fk_anlage_type) > 0) { $anlage->fk_system = $type->fk_system; } // 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; } } $anlage->setFieldValues($fieldValues); $result = $anlage->update($user); if ($result > 0) { setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); exit; } else { setEventMessages($anlage->error, $anlage->errors, 'errors'); $action = 'edit'; } } if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) { $anlage->fetch($anlageId); $systemIdBefore = $anlage->fk_system; $result = $anlage->delete($user); if ($result > 0) { setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); } else { setEventMessages($anlage->error, $anlage->errors, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemIdBefore); exit; } // File upload if ($action == 'uploadfile' && $permissiontoadd) { $anlage->fetch($anlageId); $upload_dir = $anlage->getFileDirectory(); // Create directory if not exists if (!is_dir($upload_dir)) { 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); // Generate thumbnail $anlagefile->generateThumbnail(); setEventMessages($langs->trans('FileUploaded'), null, 'mesgs'); } } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); exit; } // File delete (after confirmation) if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete) { $fileId = GETPOSTINT('fileid'); if ($fileId > 0) { $anlagefile = new AnlageFile($db); if ($anlagefile->fetch($fileId) > 0) { // Delete method handles physical file and database if ($anlagefile->delete($user) > 0) { setEventMessages($langs->trans('FileDeleted'), null, 'mesgs'); } else { setEventMessages($langs->trans('Error'), null, 'errors'); } } } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); exit; } /* * View */ // Use Dolibarr standard button classes $title = $langs->trans('TechnicalInstallations').' - '.$object->name; 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 = societe_prepare_head($object); print dol_get_fiche_head($head, 'anlagen', $langs->trans("ThirdParty"), -1, 'company'); // Thirdparty card $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom'); print '
'; // Confirmation dialogs if ($action == 'delete') { print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$id.'&anlage_id='.$anlageId.'&system='.$systemId, $langs->trans('DeleteElement'), $langs->trans('ConfirmDeleteElement'), 'confirm_delete', '', 'yes', 1 ); } if ($action == 'remove_system') { $removeSystemId = GETPOSTINT('remove_system_id'); $sysLabel = isset($customerSystems[$removeSystemId]) ? $customerSystems[$removeSystemId]->label : ''; print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$id.'&remove_system_id='.$removeSystemId, $langs->trans('RemoveSystem'), $langs->trans('ConfirmRemoveSystem', $sysLabel), 'confirm_remove_system', '', 'yes', 1 ); } if ($action == 'askdeletefile') { $fileId = GETPOSTINT('fileid'); print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&anlage_id='.$anlageId.'&fileid='.$fileId, $langs->trans('Delete'), $langs->trans('ConfirmDeleteFile'), 'confirm_deletefile', '', 'yes', 1 ); } // System tabs (only show enabled systems for this customer) print '
'; print '
'; foreach ($customerSystems as $sysId => $sys) { $activeClass = ($sysId == $systemId) ? ' active' : ''; print '
'; print ''; if ($sys->picto) { print ''.kundenkarte_render_icon($sys->picto).''; } print ''.dol_escape_htmltag($sys->label).''; print ''; // Remove button (only if no elements) if ($permissiontodelete && $sysId == $systemId) { print ' '; } print '
'; } // 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) { print '
'; print ''; print ''; if ($systemId > 0) { $exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId; print ''; print ' PDF Export'; print ''; } print '
'; } print '
'; // End kundenkarte-system-tabs-wrapper // Add system form (hidden by default) if ($permissiontoadd && !empty($availableSystems)) { print ''; } // Check if customer has any systems if (empty($customerSystems)) { print '
'; print '
'; print $langs->trans('NoSystemsConfigured').'

'; if ($permissiontoadd && !empty($allSystems)) { print $langs->trans('ClickAddSystemToStart'); } else { print $langs->trans('ContactAdminToAddSystems'); } 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 ''; print ''; // 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 ''; } else { $value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : ''; if ($value !== '') { print ''; // Format date fields if ($field->field_type === 'date' && $value) { print ''; } elseif ($field->field_type === 'checkbox') { print ''; } else { print ''; } } } } if ($anlage->note_private) { print ''; print ''; } // Creation date if ($anlage->date_creation) { print ''; print ''; } // Last modification date if ($anlage->tms && $anlage->tms != $anlage->date_creation) { print ''; print ''; } print '
'.$langs->trans('Type').''.dol_escape_htmltag($anlage->type_label).'
'.dol_escape_htmltag($field->field_label).'
'.dol_escape_htmltag($field->field_label).''.dol_print_date(strtotime($value), 'day').'
'.($value ? $langs->trans('Yes') : $langs->trans('No')).'
'.dol_escape_htmltag($value).'
'.$langs->trans('FieldNotes').''.dol_htmlentitiesbr($anlage->note_private).'
'.$langs->trans('DateCreation').''.dol_print_date($anlage->date_creation, 'dayhour').'
'.$langs->trans('DateLastModification').''.dol_print_date($anlage->tms, 'dayhour').'
'; // Files section $anlagefile = new AnlageFile($db); $files = $anlagefile->fetchAllByAnlage($anlageId); print '

'.$langs->trans('AttachedFiles').'

'; if ($permissiontoadd) { print '
'; print ''; print ''; print ' '; print '

'; } if (!empty($files)) { print '
'; foreach ($files as $file) { print '
'; print '
'; if ($file->file_type == 'image') { $thumbUrl = $file->getThumbUrl(); if ($thumbUrl) { print ''; } else { print ''; } } elseif ($file->file_type == 'pdf') { // PDF preview using iframe - 50% smaller, no toolbar print '
'; print ''; print '
'; } else { print ''; } print '
'; print '
'; print '
'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'
'; print '
'.dol_print_size($file->filesize).'
'; print '
'; print ''; if ($permissiontodelete) { print ''; } print '
'; print '
'; print '
'; } print '
'; } else { print '

'.$langs->trans('NoFiles').'

'; } // Equipment section (only if type can have equipment) if ($type->can_have_equipment) { print '

'.$langs->trans('Equipment').' - Schaltplan

'; // Equipment container - nur noch der SchematicEditor print '
'; // Schematic Editor - Hauptansicht 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 ''; // 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) { print ''.$langs->trans('Modify').''; print ''.$langs->trans('Copy').''; } if ($permissiontodelete) { print ''.$langs->trans('Delete').''; } print ''.$langs->trans('Back').''; print '
'; } else { // Create/Edit/Copy form $isEdit = ($action == 'edit'); $isCopy = ($action == 'copy'); $formAction = $isEdit ? 'update' : 'add'; print '
'; print ''; print ''; if ($isEdit) { print ''; } if ($isCopy) { print ''; } print ''; // Label $labelValue = ''; if ($isEdit) { $labelValue = $anlage->label; } elseif ($isCopy) { $labelValue = $anlage->label.' (Kopie)'; } else { $labelValue = GETPOST('label'); } print ''; print ''; // Type print ''; print ''; // Parent - for copy, use same parent as original $tree = $anlage->fetchTree($id, $systemId); $selectedParent = $isEdit || $isCopy ? $anlage->fk_parent : $parentId; $excludeId = $isEdit ? $anlageId : 0; print ''; print ''; // Dynamic fields will be inserted here via JavaScript print ''; // Notes (always at the end) print ''; $noteValue = ($isEdit || $isCopy) ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : ''); print ''; print '
'.$langs->trans('Label').'
'.$langs->trans('Type').''; if (empty($types)) { print '
'.$langs->trans('NoTypesDefinedForSystem').''; } print '
'.$langs->trans('SelectParent').'
'.$langs->trans('FieldNotes').'
'; print '
'; print ''; print ' '.$langs->trans('Cancel').''; print '
'; print '
'; } print '
'; } else { // Tree view if ($permissiontoadd) { print '
'; print ''; print ' '.$langs->trans('AddElement'); print ''; print '
'; } // 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); } if (!empty($tree)) { print '
'; printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap); print '
'; } else { print '
'.$langs->trans('NoInstallations').'
'; } } } 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()) { 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) $treeInfo = array(); 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 if ($fieldDef->field_type === 'date' && $value) { $treeInfo[] = dol_print_date(strtotime($value), 'day'); } else { $treeInfo[] = $value; } } } } 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 tree info in parentheses + file indicators $viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id; print ''.dol_escape_htmltag($node->label); if (!empty($treeInfo)) { print ' ('.dol_escape_htmltag(implode(', ', $treeInfo)).')'; } // File indicators - directly after parentheses if ($node->image_count > 0 || $node->doc_count > 0) { print ' '; if ($node->image_count > 0) { print ''; print ''; if ($node->image_count > 1) { print ' '.$node->image_count; } print ''; } if ($node->doc_count > 0) { print ''; print ''; if ($node->doc_count > 1) { print ' '.$node->doc_count; } print ''; } 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 ''; } if ($canDelete) { print ''; } print ''; print '
'; // Children if ($hasChildren) { print '
'; printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap); print '
'; } print '
'; } } /** * Print tree options for select */ function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '') { foreach ($nodes as $node) { if ($node->id == $excludeId) continue; $sel = ($node->id == $selected) ? ' selected' : ''; print ''; if (!empty($node->children)) { printTreeOptions($node->children, $selected, $excludeId, $prefix.'   '); } } } /** * Render panel HTML with its carriers * * @param EquipmentPanel $panel Panel object * @param Translate $langs Language object * @param bool $canEdit Can edit permission * @param bool $canDelete Can delete permission * @param DoliDB $db Database handler * @return string HTML output */ function renderPanelHTML($panel, $langs, $canEdit, $canDelete, $db) { $html = '
'; // Panel header $html .= '
'; $html .= ''.dol_escape_htmltag($panel->label ?: $langs->trans('PanelLabel')).''; $html .= ''; if ($canEdit) { $html .= ' '; $html .= ' '; } if ($canDelete) { $html .= ' '; } $html .= ''; $html .= '
'; // Load and render carriers for this panel $panel->fetchCarriers(); $html .= '
'; if (empty($panel->carriers)) { $html .= '
'.$langs->trans('NoCarriers').'
'; } else { foreach ($panel->carriers as $carrier) { $carrier->fetchEquipment(); $html .= renderCarrierHTML($carrier, $langs, $canEdit, $canDelete); } } // Quick-duplicate last carrier button if ($canEdit && !empty($panel->carriers)) { $lastCarrier = end($panel->carriers); $html .= '
'; $html .= ''; $html .= '
'; } $html .= '
'; $html .= '
'; return $html; } /** * Render carrier HTML for equipment display * * @param EquipmentCarrier $carrier Carrier object with equipment loaded * @param Translate $langs Language object * @param bool $canEdit Can edit permission * @param bool $canDelete Can delete permission * @return string HTML output */ function renderCarrierHTML($carrier, $langs, $canEdit, $canDelete) { global $db; $TE_WIDTH = 50; // Width of one TE in pixels (wider for more space) $BLOCK_HEIGHT = 110; // Height of equipment block $BRACKET_HEIGHT = 28; // Height reserved for protection brackets (top) $RAIL_HEIGHT = 50; // Height for rail brackets $OUTPUT_HEIGHT = 60; // Height for output labels (includes medium info) $PADDING_LEFT = 10; // Left padding $PADDING_RIGHT = 10; // Right padding $totalWidth = $carrier->total_te * $TE_WIDTH + $PADDING_LEFT + $PADDING_RIGHT; // Load connections (rails and outputs) require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php'; $connectionObj = new EquipmentConnection($db); $connections = $connectionObj->fetchByCarrier($carrier->id); // Separate rails and outputs $rails = array(); $outputs = array(); $equipmentById = array(); if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { $equipmentById[$eq->id] = $eq; } } $railsAbove = array(); $railsBelow = array(); foreach ($connections as $conn) { if ($conn->is_rail) { if ($conn->position_y < 0) { $railsAbove[] = $conn; } else { $railsBelow[] = $conn; } } } $rails = array_merge($railsAbove, $railsBelow); // Load outputs for equipment if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { $eqOutputs = $connectionObj->fetchOutputs($eq->id); foreach ($eqOutputs as $out) { $out->source_pos = $eq->position_te; $out->source_width = $eq->width_te; $outputs[] = $out; } } } // Group equipment by protection device to calculate brackets $protectionGroups = array(); if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { if ($eq->fk_protection > 0) { if (!isset($protectionGroups[$eq->fk_protection])) { $protectionGroups[$eq->fk_protection] = array( 'label' => $eq->protection_label ?: '', 'equipment' => array(), 'protection_device' => null ); if (isset($equipmentById[$eq->fk_protection])) { $protectionGroups[$eq->fk_protection]['protection_device'] = $equipmentById[$eq->fk_protection]; } } $protectionGroups[$eq->fk_protection]['equipment'][] = $eq; } } } $hasProtectionGroups = !empty($protectionGroups); $hasRailsAbove = !empty($railsAbove); $hasRailsBelow = !empty($railsBelow); $hasOutputs = !empty($outputs); // Calculate SVG height and offsets // New layout: Outputs (top) -> Protection brackets -> Equipment -> Rails (bottom) $svgHeight = $BLOCK_HEIGHT; $blockYOffset = 0; // Space for outputs ABOVE equipment (at the top) if ($hasOutputs) { $svgHeight += $OUTPUT_HEIGHT; $blockYOffset += $OUTPUT_HEIGHT; } // Space for rails above (if any) if ($hasRailsAbove) { $svgHeight += $RAIL_HEIGHT; $blockYOffset += $RAIL_HEIGHT; } // Space for FI protection brackets above equipment if ($hasProtectionGroups) { $svgHeight += $BRACKET_HEIGHT; $blockYOffset += $BRACKET_HEIGHT; } // Space for rails below equipment if ($hasRailsBelow) { $svgHeight += $RAIL_HEIGHT; } // Add extra padding at bottom for rail labels $svgHeight += 15; $html = '
'; // Header $html .= '
'; $html .= ''.dol_escape_htmltag($carrier->label ?: $langs->trans('CarrierLabel')).''; $html .= ''.$carrier->getUsedTE().'/'.$carrier->total_te.' TE '.$langs->trans('UsedTE').''; $html .= ''; if ($canEdit) { $html .= ''; $html .= ''; } if ($canDelete) { $html .= ''; } $html .= ''; $html .= '
'; // SVG Rail $html .= '
'; $html .= ''; // Draw protection brackets (FI brackets) // These are drawn just above the equipment blocks $protBracketYStart = 0; if ($hasOutputs) $protBracketYStart += $OUTPUT_HEIGHT; if ($hasRailsAbove) $protBracketYStart += $RAIL_HEIGHT; foreach ($protectionGroups as $protId => $group) { if (count($group['equipment']) > 0) { // Find leftmost and rightmost positions (including FI device) $minPos = PHP_INT_MAX; $maxPos = 0; // Include the protection device (FI) in the bracket if (!empty($group['protection_device'])) { $fi = $group['protection_device']; if ($fi->position_te < $minPos) $minPos = $fi->position_te; $endPos = $fi->position_te + $fi->width_te - 1; if ($endPos > $maxPos) $maxPos = $endPos; } // Include all protected equipment foreach ($group['equipment'] as $eq) { if ($eq->position_te < $minPos) $minPos = $eq->position_te; $endPos = $eq->position_te + $eq->width_te - 1; if ($endPos > $maxPos) $maxPos = $endPos; } $bracketX1 = $PADDING_LEFT + ($minPos - 1) * $TE_WIDTH + 2; $bracketX2 = $PADDING_LEFT + $maxPos * $TE_WIDTH - 2; $bracketWidth = $bracketX2 - $bracketX1; $bracketTopY = $protBracketYStart + 3; $bracketBottomY = $protBracketYStart + $BRACKET_HEIGHT - 2; // Draw bracket line with corners (opens downward toward equipment) $html .= 'total_te; $i++) { $x = $PADDING_LEFT + $i * $TE_WIDTH; $html .= ''; } // Track occupied slots $occupiedSlots = array(); // Render equipment blocks if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { // Mark slots as occupied for ($i = 0; $i < $eq->width_te; $i++) { $occupiedSlots[$eq->position_te + $i] = true; } $x = $PADDING_LEFT + ($eq->position_te - 1) * $TE_WIDTH; $width = $eq->width_te * $TE_WIDTH; $color = $eq->getBlockColor() ?: '#3498db'; $label = $eq->getBlockLabel() ?: ($eq->type_label_short ?: ''); // Build tooltip data $fieldValues = $eq->getFieldValues(); $tooltipData = array( 'label' => $eq->label, 'type' => $eq->type_label, 'fields' => $fieldValues, 'protection' => $eq->protection_device_label ?: '' ); $html .= ''; // Label text (centered) $fontSize = $width < 50 ? 12 : 15; $html .= ''; $html .= dol_escape_htmltag($label); $html .= ''; $html .= ''; } } // Helper function to render a rail $renderRail = function($rail, $yStart, $isAbove, $equipmentList, $teWidth, $railHeight, $paddingLeft) { $html = ''; $x1 = $paddingLeft + ($rail->rail_start_te - 1) * $teWidth + 5; $x2 = $paddingLeft + ($rail->rail_end_te) * $teWidth - 5; $railWidth = $x2 - $x1; $color = $rail->getColor(); // Parse excluded TE positions for gaps (e.g., for FI switches) $excludedPositions = array(); if (!empty($rail->excluded_te)) { $excludedPositions = array_map('intval', explode(',', $rail->excluded_te)); } // Check for multi-phase rail $railPhases = $rail->rail_phases ?: ''; $phaseColors = array(); if ($railPhases === '3P' || $railPhases === '3P+N' || $railPhases === '3P+N+PE') { $phaseColors = array( array('color' => '#8B4513', 'label' => 'L1'), // Brown array('color' => '#1a1a1a', 'label' => 'L2'), // Black array('color' => '#666666', 'label' => 'L3'), // Grey ); if ($railPhases === '3P+N' || $railPhases === '3P+N+PE') { $phaseColors[] = array('color' => '#0066CC', 'label' => 'N'); // Blue } } elseif ($railPhases === 'L1' || $railPhases === 'L1N') { $phaseColors = array( array('color' => '#8B4513', 'label' => 'L1'), ); if ($railPhases === 'L1N') { $phaseColors[] = array('color' => '#0066CC', 'label' => 'N'); } } $html .= ''; if ($isAbove) { $bracketY1 = $yStart + $railHeight - 2; $bracketY2 = $yStart + 8; $labelY = $yStart + 18; } else { $bracketY1 = $yStart + 5; $bracketY2 = $yStart + $railHeight - 8; $labelY = $bracketY2 + 14; } // Multi-phase rendering: multiple parallel horizontal lines if (!empty($phaseColors)) { $lineSpacing = 4; $totalHeight = (count($phaseColors) - 1) * $lineSpacing; $baseY = $bracketY2 - $totalHeight / 2; // Excluded positions: For FI gaps, only N is excluded, L1/L2/L3 pass through // Format: "4" = N excluded at TE 4, "4,5" = N excluded at TE 4 and 5 $nExcludedAtTE = $excludedPositions; // These positions have N gaps // Draw each phase line (L1, L2, L3 always continuous, N has gaps) foreach ($phaseColors as $idx => $phase) { $lineY = $baseY + ($idx * $lineSpacing); $phaseLabel = $phase['label']; if ($phaseLabel === 'N' && !empty($nExcludedAtTE)) { // N line has gaps at excluded positions $segments = array(); $segStart = $rail->rail_start_te; for ($te = $rail->rail_start_te; $te <= $rail->rail_end_te; $te++) { if (in_array($te, $nExcludedAtTE)) { if ($segStart < $te) { $segments[] = array('start' => $segStart, 'end' => $te - 1); } $segStart = $te + 1; } } if ($segStart <= $rail->rail_end_te) { $segments[] = array('start' => $segStart, 'end' => $rail->rail_end_te); } foreach ($segments as $seg) { $segX1 = $paddingLeft + ($seg['start'] - 1) * $teWidth + 5; $segX2 = $paddingLeft + ($seg['end']) * $teWidth - 5; $html .= ''; } } else { // L1, L2, L3 are continuous (no gaps) $html .= ''; } // Vertical end caps $html .= ''; $html .= ''; } // Vertical taps to equipment - individual colored taps per pole if (!empty($equipmentList)) { foreach ($equipmentList as $eq) { $eqEnd = $eq->position_te + $eq->width_te - 1; if ($eq->position_te >= $rail->rail_start_te && $eqEnd <= $rail->rail_end_te) { $eqCenter = $paddingLeft + ($eq->position_te - 1) * $teWidth + ($eq->width_te * $teWidth / 2); // Determine pole count from equipment $poleCount = 3; // Default: 3-pole (HS, LS etc.) $typeLabel = strtolower($eq->type_label ?? ''); $typeRef = strtolower($eq->type_ref ?? ''); // FI/RCD = 4-pole (needs N) if (strpos($typeLabel, 'fi') !== false || strpos($typeRef, 'fi') !== false || strpos($typeLabel, 'rcd') !== false) { $poleCount = 4; } // Check field values for explicit pole setting if (!empty($eq->field_values)) { $fields = is_array($eq->field_values) ? $eq->field_values : @json_decode($eq->field_values, true); if (isset($fields['pole']) && intval($fields['pole']) > 0) { $poleCount = intval($fields['pole']); } } // Is N excluded at this equipment's position? $nExcludedHere = in_array($eq->position_te, $nExcludedAtTE); // Draw individual colored taps, spread across equipment width $numTaps = min($poleCount, count($phaseColors)); $tapSpacing = min(4, ($eq->width_te * $teWidth - 10) / max(1, $numTaps - 1)); $tapStartX = $eqCenter - (($numTaps - 1) * $tapSpacing / 2); for ($p = 0; $p < $numTaps; $p++) { $phase = $phaseColors[$p]; $lineY = $baseY + ($p * $lineSpacing); $tapX = $tapStartX + ($p * $tapSpacing); // Skip N tap if N is excluded here (FI gap position) if ($phase['label'] === 'N' && $nExcludedHere) { continue; } // Vertical tap line $html .= ''; // Connection dot $html .= ''; } } } } } else { // Single line rail (original U-bracket style) - with gap support $strokeWidth = 3; $connType = strtoupper($rail->connection_type ?: ''); if (strpos($connType, 'L1L2L3') !== false || strpos($connType, '3P') !== false) { $strokeWidth = 5; if (strpos($connType, 'N') !== false) $strokeWidth = 6; if (strpos($connType, 'PE') !== false) $strokeWidth = 7; } if (empty($excludedPositions)) { // No gaps - draw simple U-bracket $html .= ''; } else { // Draw with gaps $segments = array(); $segStart = $rail->rail_start_te; for ($te = $rail->rail_start_te; $te <= $rail->rail_end_te; $te++) { if (in_array($te, $excludedPositions)) { if ($segStart < $te) { $segments[] = array('start' => $segStart, 'end' => $te - 1); } $segStart = $te + 1; } } if ($segStart <= $rail->rail_end_te) { $segments[] = array('start' => $segStart, 'end' => $rail->rail_end_te); } // Draw vertical lines at ends $html .= ''; $html .= ''; // Draw horizontal segments foreach ($segments as $seg) { $segX1 = $paddingLeft + ($seg['start'] - 1) * $teWidth + 5; $segX2 = $paddingLeft + ($seg['end']) * $teWidth - 5; $html .= ''; } } // Vertical taps to equipment if (!empty($equipmentList)) { foreach ($equipmentList as $eq) { $eqEnd = $eq->position_te + $eq->width_te - 1; $eqExcluded = false; for ($p = $eq->position_te; $p <= $eqEnd; $p++) { if (in_array($p, $excludedPositions)) { $eqExcluded = true; break; } } if (!$eqExcluded && $eq->position_te >= $rail->rail_start_te && $eqEnd <= $rail->rail_end_te) { $eqCenter = $paddingLeft + ($eq->position_te - 1) * $teWidth + ($eq->width_te * $teWidth / 2); $html .= ''; } } } } // Label - show rail_phases if set, otherwise connection_type $labelText = $railPhases ?: $rail->connection_type; if ($labelText) { $labelX = $x1 + $railWidth / 2; $labelColor = !empty($phaseColors) ? '#888' : $color; $html .= ''; $html .= dol_escape_htmltag($labelText); $html .= ''; } $html .= ''; return $html; }; // === RENDER ORDER: Outputs (top) -> Rails above -> Protection brackets -> Equipment -> Rails below === // 1. Render OUTPUTS at the TOP (above everything else) if ($hasOutputs) { $outputY = 0; foreach ($outputs as $out) { $eqCenter = $PADDING_LEFT + ($out->source_pos - 1) * $TE_WIDTH + ($out->source_width * $TE_WIDTH / 2); $color = $out->getColor(); $html .= ''; // Label at top $displayLabel = $out->output_label ?: ''; if ($displayLabel) { // Medium info as smaller text $mediumInfo = ''; if ($out->medium_type) { $mediumInfo = $out->medium_type; if ($out->medium_spec) $mediumInfo .= ' '.$out->medium_spec; } // Main label - larger font, more spacing $html .= ''; $html .= dol_escape_htmltag($displayLabel); $html .= ''; // Medium info below label if ($mediumInfo) { $html .= ''; $html .= dol_escape_htmltag($mediumInfo); $html .= ''; } } // Vertical line from label DOWN to equipment block $lineStartY = $outputY + $OUTPUT_HEIGHT - 8; $lineEndY = $blockYOffset; $html .= ''; $html .= ''; } } // 2. Render rails ABOVE equipment (if any) if ($hasRailsAbove) { $railAboveYStart = $hasOutputs ? $OUTPUT_HEIGHT : 0; foreach ($railsAbove as $rail) { $html .= $renderRail($rail, $railAboveYStart, true, $carrier->equipment, $TE_WIDTH, $RAIL_HEIGHT, $PADDING_LEFT); } } // 3. Render rails BELOW equipment (at the bottom) $currentY = $blockYOffset + $BLOCK_HEIGHT; if ($hasRailsBelow) { foreach ($railsBelow as $rail) { $html .= $renderRail($rail, $currentY, false, $carrier->equipment, $TE_WIDTH, $RAIL_HEIGHT, $PADDING_LEFT); } } $html .= ''; // Clickable slots overlay (for adding equipment) if ($canEdit) { $html .= '
'; // 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 .= '
width_te * $TE_WIDTH).'px;" '; $html .= 'title="'.$langs->trans('DuplicatePrevious').'">'; $html .= ''; $html .= '
'; // Skip the slots that would be covered by this button $pos += $lastEquipment->width_te - 1; } else { // Regular empty slot $html .= '
'; } } } $html .= '
'; } $html .= '
'; // svg-container // Connection Editor Wrapper $html .= '
'; // Toggle button and action buttons $html .= '
'; $html .= ''; $html .= ' '.$langs->trans('ConnectionEditor'); $html .= ''; if ($canEdit) { $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; } $html .= '
'; // header // Editor canvas area (initially hidden) $totalWidth = $carrier->total_te * $TE_WIDTH; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; // editor $html .= '
'; // editor-wrapper $html .= '
'; // carrier return $html; } /** * Render connections HTML for a carrier * * @param EquipmentCarrier $carrier Carrier object * @param Translate $langs Language object * @param bool $canEdit Can edit permission * @param int $teWidth Width of one TE in pixels * @return string HTML output */ function renderConnectionsHTML($carrier, $langs, $canEdit, $teWidth) { global $db; require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php'; $connectionObj = new EquipmentConnection($db); $connections = $connectionObj->fetchByCarrier($carrier->id); // Also get outputs for equipment on this carrier $outputs = array(); if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { $eqOutputs = $connectionObj->fetchOutputs($eq->id); foreach ($eqOutputs as $out) { $out->source_pos = $eq->position_te; $out->source_width = $eq->width_te; $outputs[] = $out; } } } // If no connections and no outputs, don't render container if (empty($connections) && empty($outputs) && !$canEdit) { return ''; } $totalWidth = $carrier->total_te * $teWidth; $outputRowHeight = 25; // Height for output labels (25% larger) $railRowHeight = 23; // Height for rail (25% larger) // Count rails and determine layout $rails = array(); $otherConnections = array(); foreach ($connections as $conn) { if ($conn->is_rail) { $rails[] = $conn; } else { $otherConnections[] = $conn; } } // Layout: Outputs (row 0) -> Rails (row 1) -> Other connections (row 2+) $hasOutputs = !empty($outputs); $hasRails = !empty($rails); // Check for multi-phase rails (need more height) $hasMultiPhaseRail = false; foreach ($rails as $r) { if (!empty($r->rail_phases) && in_array($r->rail_phases, array('3P', '3P+N', '3P+N+PE'))) { $hasMultiPhaseRail = true; break; } } $actualRailHeight = $hasMultiPhaseRail ? ($railRowHeight + 8) : $railRowHeight; $svgHeight = 10; if ($hasOutputs) $svgHeight += $outputRowHeight; if ($hasRails) $svgHeight += $actualRailHeight; if ($canEdit && !$hasOutputs && !$hasRails) $svgHeight = 30; $html = '
'; $html .= ''; // Y positions $outputY = 5; // Outputs at top $railY = $hasOutputs ? ($outputRowHeight + 8) : 10; // 1. Render outputs (Abgänge) - at top, closest to equipment foreach ($outputs as $out) { $eqCenter = ($out->source_pos - 1) * $teWidth + ($out->source_width * $teWidth / 2); $color = $out->getColor(); $html .= ''; // Small vertical line down from equipment $html .= ''; // Label $displayLabel = $out->getDisplayLabel(); if ($displayLabel) { $html .= ''; $html .= dol_escape_htmltag($displayLabel); $html .= ''; } $html .= ''; } // 2. Render rails (Sammelschienen) - below outputs foreach ($rails as $conn) { $x1 = ($conn->rail_start_te - 1) * $teWidth + $teWidth/2; $x2 = ($conn->rail_end_te - 1) * $teWidth + $teWidth/2; $color = $conn->getColor(); // Parse excluded TE positions $excludedPositions = array(); if (!empty($conn->excluded_te)) { $excludedPositions = array_map('intval', explode(',', $conn->excluded_te)); } $html .= ''; // Check for multi-phase rail $railPhases = $conn->rail_phases ?: ''; $phaseLines = array(); if ($railPhases === '3P' || $railPhases === '3P+N' || $railPhases === '3P+N+PE') { $phaseLines = array( array('color' => '#8B4513', 'label' => 'L1'), // Brown array('color' => '#1a1a1a', 'label' => 'L2'), // Black array('color' => '#666666', 'label' => 'L3'), // Grey ); if ($railPhases === '3P+N' || $railPhases === '3P+N+PE') { $phaseLines[] = array('color' => '#0066CC', 'label' => 'N'); // Blue } } elseif ($railPhases === 'L1' || $railPhases === 'L1N') { $phaseLines = array( array('color' => '#8B4513', 'label' => 'L1'), ); if ($railPhases === 'L1N') { $phaseLines[] = array('color' => '#0066CC', 'label' => 'N'); } } // If multi-phase, draw parallel lines if (!empty($phaseLines)) { $lineSpacing = 3; // Space between lines $totalPhaseHeight = (count($phaseLines) - 1) * $lineSpacing; $startY = $railY - $totalPhaseHeight / 2; foreach ($phaseLines as $idx => $phase) { $lineY = $startY + ($idx * $lineSpacing); // Draw line segments (with gaps for excluded positions) $segments = array(); $segmentStart = $conn->rail_start_te; for ($te = $conn->rail_start_te; $te <= $conn->rail_end_te; $te++) { if (in_array($te, $excludedPositions)) { // Close current segment before excluded position if ($segmentStart < $te) { $segments[] = array('start' => $segmentStart, 'end' => $te - 1); } $segmentStart = $te + 1; } } // Add final segment if ($segmentStart <= $conn->rail_end_te) { $segments[] = array('start' => $segmentStart, 'end' => $conn->rail_end_te); } // Draw each segment foreach ($segments as $seg) { $segX1 = ($seg['start'] - 1) * $teWidth + $teWidth/2; $segX2 = ($seg['end'] - 1) * $teWidth + $teWidth/2; $html .= ''; } } } else { // Single line rail (original behavior) - also with exclusions $segments = array(); $segmentStart = $conn->rail_start_te; for ($te = $conn->rail_start_te; $te <= $conn->rail_end_te; $te++) { if (in_array($te, $excludedPositions)) { if ($segmentStart < $te) { $segments[] = array('start' => $segmentStart, 'end' => $te - 1); } $segmentStart = $te + 1; } } if ($segmentStart <= $conn->rail_end_te) { $segments[] = array('start' => $segmentStart, 'end' => $conn->rail_end_te); } foreach ($segments as $seg) { $segX1 = ($seg['start'] - 1) * $teWidth + $teWidth/2; $segX2 = ($seg['end'] - 1) * $teWidth + $teWidth/2; $html .= ''; } } // Draw taps (small vertical lines connecting to equipment) if (!empty($carrier->equipment)) { foreach ($carrier->equipment as $eq) { $eqCenter = ($eq->position_te - 1) * $teWidth + ($eq->width_te * $teWidth / 2); // Check if equipment is in rail range and not excluded $eqInRange = $eqCenter >= $x1 && $eqCenter <= $x2; $eqNotExcluded = !in_array($eq->position_te, $excludedPositions); if ($eqInRange && $eqNotExcluded) { $tapColor = !empty($phaseLines) ? '#666' : $color; $html .= ''; } } } // Label for rail type $labelText = $conn->connection_type; if ($railPhases) { $labelText = $railPhases; } if ($labelText) { $labelX = ($x1 + $x2) / 2; $labelColor = !empty($phaseLines) ? '#555' : $color; $html .= ''; $html .= dol_escape_htmltag($labelText); $html .= ''; } $html .= ''; } // Render regular connections (equipment to equipment) foreach ($connections as $conn) { if (!$conn->is_rail && $conn->fk_source > 0 && $conn->fk_target > 0) { // Find source and target positions $sourcePos = $conn->source_pos; $targetPos = $conn->target_pos; if ($sourcePos && $targetPos) { $x1 = ($sourcePos - 1) * $teWidth + $teWidth/2; $x2 = ($targetPos - 1) * $teWidth + $teWidth/2; $y = $conn->position_y * $rowHeight + 12; $color = $conn->getColor(); $html .= ''; $html .= ''; $html .= ''; } } } $html .= ''; // Add connection buttons if ($canEdit) { $html .= '
'; $html .= ''; $html .= ' '.$langs->trans('AddOutput'); $html .= ''; $html .= ''; $html .= ' '.$langs->trans('AddRail'); $html .= ''; $html .= '
'; } $html .= '
'; return $html; }