2248 lines
86 KiB
PHP
Executable file
2248 lines
86 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Alles Watt lauft
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*/
|
|
|
|
/**
|
|
* \file tabs/anlagen.php
|
|
* \brief Tab for technical installations on thirdparty card
|
|
*/
|
|
|
|
// Load Dolibarr environment
|
|
$res = 0;
|
|
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
|
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
|
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
|
if (!$res) die("Include of main fails");
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
|
dol_include_once('/kundenkarte/class/anlagetype.class.php');
|
|
dol_include_once('/kundenkarte/class/anlagefile.class.php');
|
|
dol_include_once('/kundenkarte/class/equipmentpanel.class.php');
|
|
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
|
|
dol_include_once('/kundenkarte/class/equipment.class.php');
|
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
|
|
|
// Load translation files
|
|
$langs->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/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 = '<a href="'.DOL_URL_ROOT.'/societe/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
|
|
dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom');
|
|
|
|
print '<div class="fichecenter">';
|
|
|
|
// 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 '<div class="kundenkarte-system-tabs-wrapper">';
|
|
print '<div class="kundenkarte-system-tabs">';
|
|
foreach ($customerSystems as $sysId => $sys) {
|
|
$activeClass = ($sysId == $systemId) ? ' active' : '';
|
|
print '<div class="kundenkarte-system-tab'.$activeClass.'" data-system="'.$sysId.'">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$sysId.'" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:8px;">';
|
|
if ($sys->picto) {
|
|
print '<span class="kundenkarte-system-tab-icon" style="color:'.$sys->color.';">'.kundenkarte_render_icon($sys->picto).'</span>';
|
|
}
|
|
print '<span>'.dol_escape_htmltag($sys->label).'</span>';
|
|
print '</a>';
|
|
// Remove button (only if no elements)
|
|
if ($permissiontodelete && $sysId == $systemId) {
|
|
print ' <a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&action=remove_system&remove_system_id='.$sysId.'" class="kundenkarte-system-remove" title="'.$langs->trans('RemoveSystem').'"><i class="fa fa-times"></i></a>';
|
|
}
|
|
print '</div>';
|
|
}
|
|
|
|
// 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 '<button type="button" class="button small" style="margin-left:auto;" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
|
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
|
|
print '</button>';
|
|
}
|
|
}
|
|
print '</div>';
|
|
|
|
// 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 '<div class="kundenkarte-tree-controls">';
|
|
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 '</button>';
|
|
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
|
|
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
|
|
print '</button>';
|
|
if ($systemId > 0) {
|
|
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId;
|
|
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
|
|
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
|
|
print '</a>';
|
|
}
|
|
print '</div>';
|
|
}
|
|
|
|
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
|
|
|
// Add system form (hidden by default)
|
|
if ($permissiontoadd && !empty($availableSystems)) {
|
|
print '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_system">';
|
|
print '<strong>'.$langs->trans('SelectSystemToAdd').':</strong> ';
|
|
print '<select name="new_system_id" class="flat">';
|
|
print '<option value="">'.$langs->trans('Select').'</option>';
|
|
foreach ($availableSystems as $avSys) {
|
|
print '<option value="'.$avSys->rowid.'">'.dol_escape_htmltag($avSys->label).'</option>';
|
|
}
|
|
print '</select>';
|
|
print ' <button type="submit" class="button small">'.$langs->trans('Add').'</button>';
|
|
print ' <button type="button" class="button small" onclick="document.getElementById(\'add-system-form\').style.display=\'none\';">'.$langs->trans('Cancel').'</button>';
|
|
print '</form>';
|
|
print '</div>';
|
|
}
|
|
|
|
// Check if customer has any systems
|
|
if (empty($customerSystems)) {
|
|
print '<div class="opacitymedium" style="padding:20px;text-align:center;">';
|
|
print '<i class="fa fa-info-circle" style="font-size:24px;margin-bottom:10px;"></i><br>';
|
|
print $langs->trans('NoSystemsConfigured').'<br><br>';
|
|
if ($permissiontoadd && !empty($allSystems)) {
|
|
print $langs->trans('ClickAddSystemToStart');
|
|
} else {
|
|
print $langs->trans('ContactAdminToAddSystems');
|
|
}
|
|
print '</div>';
|
|
} 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 '<div class="kundenkarte-element-form">';
|
|
|
|
if ($action == 'view') {
|
|
// View mode
|
|
print '<h3>'.dol_escape_htmltag($anlage->label).'</h3>';
|
|
|
|
print '<table class="border centpercent">';
|
|
|
|
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
|
|
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
|
|
|
|
// 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 '<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>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($anlage->note_private) {
|
|
print '<tr><td>'.$langs->trans('FieldNotes').'</td>';
|
|
print '<td>'.dol_htmlentitiesbr($anlage->note_private).'</td></tr>';
|
|
}
|
|
|
|
// Creation date
|
|
if ($anlage->date_creation) {
|
|
print '<tr><td>'.$langs->trans('DateCreation').'</td>';
|
|
print '<td>'.dol_print_date($anlage->date_creation, 'dayhour').'</td></tr>';
|
|
}
|
|
|
|
// Last modification date
|
|
if ($anlage->tms && $anlage->tms != $anlage->date_creation) {
|
|
print '<tr><td>'.$langs->trans('DateLastModification').'</td>';
|
|
print '<td>'.dol_print_date($anlage->tms, 'dayhour').'</td></tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
|
|
// Files section
|
|
$anlagefile = new AnlageFile($db);
|
|
$files = $anlagefile->fetchAllByAnlage($anlageId);
|
|
|
|
print '<br><h4>'.$langs->trans('AttachedFiles').'</h4>';
|
|
|
|
if ($permissiontoadd) {
|
|
print '<form method="POST" enctype="multipart/form-data" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=uploadfile&anlage_id='.$anlageId.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="file" name="userfile" accept="image/*,.pdf,.doc,.docx">';
|
|
print ' <button type="submit" class="button">'.$langs->trans('Upload').'</button>';
|
|
print '</form><br>';
|
|
}
|
|
|
|
if (!empty($files)) {
|
|
print '<div class="kundenkarte-files-grid">';
|
|
foreach ($files as $file) {
|
|
print '<div class="kundenkarte-file-item">';
|
|
print '<div class="kundenkarte-file-preview">';
|
|
if ($file->file_type == 'image') {
|
|
$thumbUrl = $file->getThumbUrl();
|
|
if ($thumbUrl) {
|
|
print '<img src="'.$thumbUrl.'" alt="">';
|
|
} else {
|
|
print '<img src="'.$file->getUrl().'" alt="" style="max-width:100%;max-height:100%;">';
|
|
}
|
|
} elseif ($file->file_type == 'pdf') {
|
|
// PDF preview using iframe - 50% smaller, no toolbar
|
|
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 '</div>';
|
|
} else {
|
|
print '<i class="fa fa-file-o" style="font-size:48px;color:#999;"></i>';
|
|
}
|
|
print '</div>';
|
|
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-size">'.dol_print_size($file->filesize).'</div>';
|
|
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>';
|
|
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 '</div>';
|
|
print '</div>';
|
|
print '</div>';
|
|
}
|
|
print '</div>';
|
|
} else {
|
|
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').' (Hutschienen)</h4>';
|
|
|
|
// Equipment container
|
|
print '<div class="kundenkarte-equipment-container" data-anlage-id="'.$anlageId.'" data-system-id="'.$systemId.'">';
|
|
|
|
// Load panels for this Anlage
|
|
$panelObj = new EquipmentPanel($db);
|
|
$panels = $panelObj->fetchByAnlage($anlageId);
|
|
|
|
// Load carriers without panel (legacy or direct carriers)
|
|
$carrierObj = new EquipmentCarrier($db);
|
|
$directCarriers = array();
|
|
$allCarriers = $carrierObj->fetchByAnlage($anlageId);
|
|
foreach ($allCarriers as $c) {
|
|
if (empty($c->fk_panel)) {
|
|
$directCarriers[] = $c;
|
|
}
|
|
}
|
|
|
|
// Panel management header
|
|
print '<div class="kundenkarte-equipment-header">';
|
|
print '<h4>'.$langs->trans('Panels').' ('.$langs->trans('Fields').')</h4>';
|
|
if ($permissiontoadd) {
|
|
print '<a href="#" class="kundenkarte-add-panel" data-anlage-id="'.$anlageId.'">';
|
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddPanel');
|
|
print '</a>';
|
|
}
|
|
print '</div>';
|
|
|
|
// Panels container (horizontal scrolling)
|
|
print '<div class="kundenkarte-panels-container">';
|
|
|
|
if (!empty($panels)) {
|
|
foreach ($panels as $p) {
|
|
print renderPanelHTML($p, $langs, $permissiontoadd, $permissiontodelete, $db);
|
|
}
|
|
// Quick-duplicate last panel button
|
|
if ($permissiontoadd) {
|
|
$lastPanel = end($panels);
|
|
print '<div class="kundenkarte-panel-quickadd" data-panel-id="'.$lastPanel->id.'" data-anlage-id="'.$anlageId.'" title="'.$langs->trans('DuplicatePreviousPanel').'">';
|
|
print '<i class="fa fa-plus"></i>';
|
|
print '</div>';
|
|
}
|
|
}
|
|
|
|
// Direct carriers (without panel) - legacy support
|
|
if (!empty($directCarriers)) {
|
|
print '<div class="kundenkarte-panel kundenkarte-panel-direct">';
|
|
print '<div class="kundenkarte-panel-header">';
|
|
print '<span class="kundenkarte-panel-label">'.$langs->trans('DirectCarriers').'</span>';
|
|
if ($permissiontoadd) {
|
|
print '<a href="#" class="kundenkarte-add-carrier" data-anlage-id="'.$anlageId.'" data-panel-id="0">';
|
|
print '<i class="fa fa-plus"></i>';
|
|
print '</a>';
|
|
}
|
|
print '</div>';
|
|
print '<div class="kundenkarte-carriers-list">';
|
|
foreach ($directCarriers as $c) {
|
|
$c->fetchEquipment();
|
|
print renderCarrierHTML($c, $langs, $permissiontoadd, $permissiontodelete);
|
|
}
|
|
print '</div>';
|
|
print '</div>';
|
|
}
|
|
|
|
// Show "Add Panel" placeholder if no panels and no direct carriers
|
|
if (empty($panels) && empty($directCarriers)) {
|
|
print '<div class="opacitymedium" style="padding:20px;">';
|
|
print $langs->trans('NoPanelsOrCarriers').' - ';
|
|
print $langs->trans('AddPanelOrCarrier');
|
|
print '</div>';
|
|
}
|
|
|
|
print '</div>'; // .kundenkarte-panels-container
|
|
|
|
// Schematic Editor - Interactive Connection Editor (always expanded)
|
|
print '<div class="schematic-editor-wrapper" style="margin-top:20px;">';
|
|
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('ConnectionEditor').'</strong> <span style="color:#888;font-size:0.85em;">(Klick auf Terminal → Klick auf Ziel-Terminal = Verbindung | Rechtsklick = Löschen)</span>';
|
|
print '</div>';
|
|
print '<div class="schematic-editor-actions" style="display:flex;gap:10px;">';
|
|
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>';
|
|
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 Equipment JavaScript
|
|
print '<script>
|
|
$(document).ready(function() {
|
|
if (typeof KundenKarte !== "undefined") {
|
|
if (KundenKarte.Equipment) {
|
|
KundenKarte.Equipment.currentAnlageId = '.$anlageId.';
|
|
}
|
|
if (KundenKarte.ConnectionEditor) {
|
|
KundenKarte.ConnectionEditor.init('.$anlageId.');
|
|
}
|
|
if (KundenKarte.SchematicEditor) {
|
|
KundenKarte.SchematicEditor.init('.$anlageId.');
|
|
}
|
|
}
|
|
});
|
|
</script>';
|
|
|
|
// === Interactive Connection Editor (Prototype) ===
|
|
// Pure SVG solution - no external libraries needed
|
|
|
|
print '<div class="kundenkarte-jsplumb-prototype" style="margin-top:20px;">';
|
|
print '<div class="titre" style="cursor:pointer;" id="connection-editor-toggle">';
|
|
print '<i class="fa fa-flask"></i> Interaktiver Verbindungseditor (Prototyp) ';
|
|
print '<i class="fa fa-chevron-down" style="font-size:12px;"></i>';
|
|
print '</div>';
|
|
|
|
print '<div id="connection-editor-container" style="display:none;margin-top:10px;padding:15px;background:#1a1a1a;border:1px solid #444;border-radius:6px;">';
|
|
|
|
print '<p style="color:#888;font-size:12px;margin-bottom:10px;">';
|
|
print '<strong>Bedienung:</strong> Elemente mit der Maus ziehen. Klick auf Ausgang (gruen) dann auf Eingang (rot) um Verbindung zu erstellen. Rechtsklick auf Verbindung zum Loeschen.';
|
|
print '</p>';
|
|
|
|
// Toolbar
|
|
print '<div style="margin-bottom:10px;display:flex;gap:10px;flex-wrap:wrap;">';
|
|
print '<button type="button" class="button" id="btn-clear-connections"><i class="fa fa-trash"></i> Alle Verbindungen loeschen</button>';
|
|
print '<button type="button" class="button" id="btn-auto-layout"><i class="fa fa-magic"></i> Auto-Layout</button>';
|
|
print '<span style="color:#666;font-size:11px;align-self:center;" id="connection-status">Bereit</span>';
|
|
print '</div>';
|
|
|
|
// Canvas area
|
|
print '<div id="connection-canvas" style="position:relative;width:100%;min-height:400px;background:linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);border:1px solid #555;border-radius:4px;overflow:visible;">';
|
|
|
|
// SVG layer for connections (drawn behind nodes)
|
|
print '<svg id="connections-svg" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;">';
|
|
// Grid pattern
|
|
print '<defs>';
|
|
print '<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">';
|
|
print '<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#fff" stroke-width="0.5" opacity="0.1"/>';
|
|
print '</pattern>';
|
|
// Arrow marker for connections
|
|
print '<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">';
|
|
print '<path d="M0,0 L0,6 L9,3 z" fill="#3498db"/>';
|
|
print '</marker>';
|
|
print '</defs>';
|
|
print '<rect width="100%" height="100%" fill="url(#grid)"/>';
|
|
// Connection lines will be added here dynamically
|
|
print '<g id="connection-lines"></g>';
|
|
print '</svg>';
|
|
|
|
// Create draggable equipment blocks
|
|
$blockY = 80;
|
|
$nodeIndex = 0;
|
|
if (!empty($allCarriers)) {
|
|
foreach ($allCarriers as $jsCarrier) {
|
|
$jsCarrier->fetchEquipment();
|
|
$blockX = 50;
|
|
if (!empty($jsCarrier->equipment)) {
|
|
foreach ($jsCarrier->equipment as $eq) {
|
|
$blockId = 'node-'.$eq->id;
|
|
$color = $eq->type_color ?: '#3498db';
|
|
$label = $eq->getBlockLabel() ?: $eq->type_label_short ?: 'EQ';
|
|
print '<div id="'.$blockId.'" class="drag-node" data-node-id="'.$eq->id.'" ';
|
|
print 'style="position:absolute;left:'.$blockX.'px;top:'.$blockY.'px;width:70px;height:90px;z-index:10;';
|
|
print 'background:'.$color.';';
|
|
print 'border:2px solid rgba(255,255,255,0.3);border-radius:8px;cursor:grab;';
|
|
print 'display:flex;flex-direction:column;align-items:center;justify-content:center;';
|
|
print 'color:#fff;font-size:12px;font-weight:bold;text-align:center;padding:5px;';
|
|
print 'box-shadow:0 4px 15px rgba(0,0,0,0.3);transition:box-shadow 0.2s, transform 0.1s;">';
|
|
// Input connector (top)
|
|
print '<div class="connector connector-in" data-type="in" style="position:absolute;top:-8px;left:50%;transform:translateX(-50%);width:20px;height:20px;background:#e74c3c;border:3px solid #fff;border-radius:50%;cursor:crosshair;z-index:100;pointer-events:auto;" title="Eingang"></div>';
|
|
print '<span style="margin-top:12px;">'.dol_escape_htmltag($label).'</span>';
|
|
print '<span style="font-size:9px;opacity:0.7;">'.dol_escape_htmltag($eq->type_label_short ?: '').'</span>';
|
|
// Output connector (bottom)
|
|
print '<div class="connector connector-out" data-type="out" style="position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);width:20px;height:20px;background:#27ae60;border:3px solid #fff;border-radius:50%;cursor:crosshair;z-index:100;pointer-events:auto;" title="Ausgang"></div>';
|
|
print '</div>';
|
|
$blockX += 90;
|
|
$nodeIndex++;
|
|
}
|
|
}
|
|
$blockY += 130;
|
|
}
|
|
}
|
|
|
|
// External input node
|
|
print '<div id="node-external" class="drag-node" data-node-id="external" ';
|
|
print 'style="position:absolute;left:50px;top:10px;width:120px;height:40px;z-index:10;';
|
|
print 'background:linear-gradient(135deg, #27ae60 0%, #1e8449 100%);';
|
|
print 'border:2px solid rgba(255,255,255,0.3);border-radius:8px;cursor:grab;';
|
|
print 'display:flex;align-items:center;justify-content:center;';
|
|
print 'color:#fff;font-size:12px;font-weight:bold;';
|
|
print 'box-shadow:0 4px 15px rgba(0,0,0,0.3);">';
|
|
print '<i class="fa fa-bolt" style="margin-right:5px;"></i> Einspeisung';
|
|
print '<div class="connector connector-out" data-type="out" style="position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);width:20px;height:20px;background:#27ae60;border:3px solid #fff;border-radius:50%;cursor:crosshair;z-index:100;pointer-events:auto;" title="Ausgang"></div>';
|
|
print '</div>';
|
|
|
|
print '</div>'; // #connection-canvas
|
|
|
|
// JavaScript for drag & drop and connections
|
|
print '<script>
|
|
/* Connection Editor v2.3 - '.date('Y-m-d H:i:s').' - flexible Richtung */
|
|
(function() {
|
|
var initialized = false;
|
|
var connections = [];
|
|
var pendingConnection = null;
|
|
|
|
function init() {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
var canvas = document.getElementById("connection-canvas");
|
|
var nodes = canvas.querySelectorAll(".drag-node");
|
|
var statusEl = document.getElementById("connection-status");
|
|
|
|
// VERSION MARKER - if you see this, cache is cleared
|
|
console.log("=== CONNECTION EDITOR v2.0 ===");
|
|
console.log("Initializing with " + nodes.length + " nodes");
|
|
statusEl.textContent = "Editor v2.0 geladen";
|
|
|
|
// Make nodes draggable
|
|
nodes.forEach(function(node) {
|
|
var isDragging = false;
|
|
var startX, startY, origX, origY;
|
|
|
|
node.addEventListener("mousedown", function(e) {
|
|
if (e.target.classList.contains("connector")) return;
|
|
isDragging = true;
|
|
node.style.cursor = "grabbing";
|
|
node.style.zIndex = 100;
|
|
node.style.transform = "scale(1.05)";
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
origX = node.offsetLeft;
|
|
origY = node.offsetTop;
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener("mousemove", function(e) {
|
|
if (!isDragging) return;
|
|
var dx = e.clientX - startX;
|
|
var dy = e.clientY - startY;
|
|
var newX = Math.max(0, Math.min(canvas.offsetWidth - node.offsetWidth, origX + dx));
|
|
var newY = Math.max(0, Math.min(canvas.offsetHeight - node.offsetHeight, origY + dy));
|
|
node.style.left = newX + "px";
|
|
node.style.top = newY + "px";
|
|
updateConnections();
|
|
});
|
|
|
|
document.addEventListener("mouseup", function() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
node.style.cursor = "grab";
|
|
node.style.zIndex = 10;
|
|
node.style.transform = "scale(1)";
|
|
}
|
|
});
|
|
});
|
|
|
|
// Connector click handling
|
|
var connectors = canvas.querySelectorAll(".connector");
|
|
console.log("[DEBUG] Found " + connectors.length + " connectors");
|
|
|
|
connectors.forEach(function(connEl) {
|
|
console.log("[DEBUG] Handler fuer Connector:", connEl.dataset.type, "auf Node", connEl.closest(".drag-node").dataset.nodeId);
|
|
|
|
// Flexibel: egal ob von oben oder unten eingespeist wird
|
|
connEl.addEventListener("mousedown", function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
var type = connEl.dataset.type;
|
|
var nodeEl = connEl.closest(".drag-node");
|
|
var nodeId = nodeEl.dataset.nodeId;
|
|
|
|
console.log("[DEBUG] Connector GEKLICKT: type=" + type + ", node=" + nodeId);
|
|
|
|
if (!pendingConnection) {
|
|
// Erster Klick - starte Verbindung (egal ob Input oder Output)
|
|
pendingConnection = { from: nodeEl, fromConn: connEl, fromType: type };
|
|
statusEl.textContent = "Von " + nodeId + " (" + type + ") - klicke auf Ziel";
|
|
statusEl.style.color = "#f39c12";
|
|
connEl.style.boxShadow = "0 0 12px #f39c12";
|
|
console.log("[DEBUG] Verbindung gestartet von " + nodeId);
|
|
} else if (pendingConnection.from === nodeEl) {
|
|
// Gleicher Node - abbrechen
|
|
console.log("[DEBUG] Gleicher Node - abgebrochen");
|
|
pendingConnection.fromConn.style.boxShadow = "";
|
|
pendingConnection = null;
|
|
statusEl.textContent = "Abgebrochen";
|
|
statusEl.style.color = "#e74c3c";
|
|
} else {
|
|
// Zweiter Klick auf anderem Node - Verbindung erstellen
|
|
console.log("[DEBUG] Erstelle Verbindung...");
|
|
createConnection(pendingConnection.from, pendingConnection.fromConn, nodeEl, connEl);
|
|
pendingConnection.fromConn.style.boxShadow = "";
|
|
pendingConnection = null;
|
|
statusEl.textContent = "Verbindung erstellt!";
|
|
statusEl.style.color = "#27ae60";
|
|
}
|
|
setTimeout(function() {
|
|
if (!pendingConnection) {
|
|
statusEl.textContent = "Bereit";
|
|
statusEl.style.color = "#666";
|
|
}
|
|
}, 1500);
|
|
});
|
|
|
|
// Hover effects
|
|
connEl.addEventListener("mouseenter", function() {
|
|
connEl.style.transform = "translateX(-50%) scale(1.3)";
|
|
});
|
|
connEl.addEventListener("mouseleave", function() {
|
|
connEl.style.transform = "translateX(-50%) scale(1)";
|
|
});
|
|
});
|
|
|
|
// Cancel pending connection on canvas click
|
|
canvas.addEventListener("click", function(e) {
|
|
if (!e.target.classList.contains("connector") && pendingConnection) {
|
|
pendingConnection.fromConn.style.boxShadow = "";
|
|
pendingConnection = null;
|
|
statusEl.textContent = "Abgebrochen";
|
|
statusEl.style.color = "#e74c3c";
|
|
setTimeout(function() {
|
|
statusEl.textContent = "Bereit";
|
|
statusEl.style.color = "#666";
|
|
}, 1000);
|
|
}
|
|
});
|
|
|
|
// Clear all connections
|
|
document.getElementById("btn-clear-connections").addEventListener("click", function() {
|
|
connections.forEach(function(c) { if (c.path) c.path.remove(); });
|
|
connections = [];
|
|
statusEl.textContent = "Alle Verbindungen geloescht";
|
|
statusEl.style.color = "#e74c3c";
|
|
setTimeout(function() { statusEl.textContent = "Bereit"; statusEl.style.color = "#666"; }, 1500);
|
|
});
|
|
|
|
// Auto layout
|
|
document.getElementById("btn-auto-layout").addEventListener("click", function() {
|
|
var x = 50, y = 80, row = 0;
|
|
nodes.forEach(function(node, i) {
|
|
if (node.dataset.nodeId === "external") {
|
|
node.style.left = "50px";
|
|
node.style.top = "10px";
|
|
} else {
|
|
node.style.left = x + "px";
|
|
node.style.top = y + "px";
|
|
x += 90;
|
|
if ((i + 1) % 8 === 0) { x = 50; y += 130; }
|
|
}
|
|
});
|
|
updateConnections();
|
|
statusEl.textContent = "Layout angepasst";
|
|
statusEl.style.color = "#3498db";
|
|
setTimeout(function() { statusEl.textContent = "Bereit"; statusEl.style.color = "#666"; }, 1500);
|
|
});
|
|
}
|
|
|
|
function createConnection(fromNode, fromConn, toNode, toConn) {
|
|
console.log("createConnection called");
|
|
try {
|
|
var svgNS = "http://www.w3.org/2000/svg";
|
|
var linesGroup = document.getElementById("connection-lines");
|
|
var canvas = document.getElementById("connection-canvas");
|
|
|
|
console.log("linesGroup:", linesGroup);
|
|
console.log("canvas:", canvas);
|
|
|
|
// Create path element
|
|
var path = document.createElementNS(svgNS, "path");
|
|
path.setAttribute("stroke", "#3498db");
|
|
path.setAttribute("stroke-width", "3");
|
|
path.setAttribute("fill", "none");
|
|
path.setAttribute("marker-end", "url(#arrow)");
|
|
path.style.pointerEvents = "stroke";
|
|
path.style.cursor = "pointer";
|
|
|
|
// Calculate path
|
|
var pathD = calculatePath(fromConn, toConn, canvas);
|
|
console.log("Path D:", pathD);
|
|
path.setAttribute("d", pathD);
|
|
|
|
linesGroup.appendChild(path);
|
|
console.log("Path added to SVG");
|
|
|
|
var conn = {
|
|
from: fromNode,
|
|
to: toNode,
|
|
fromConn: fromConn,
|
|
toConn: toConn,
|
|
path: path
|
|
};
|
|
connections.push(conn);
|
|
|
|
// Hover effect
|
|
path.addEventListener("mouseenter", function() {
|
|
path.setAttribute("stroke", "#e74c3c");
|
|
path.setAttribute("stroke-width", "4");
|
|
});
|
|
path.addEventListener("mouseleave", function() {
|
|
path.setAttribute("stroke", "#3498db");
|
|
path.setAttribute("stroke-width", "3");
|
|
});
|
|
|
|
// Right-click to delete
|
|
path.addEventListener("contextmenu", function(e) {
|
|
e.preventDefault();
|
|
var idx = connections.indexOf(conn);
|
|
if (idx >= 0) {
|
|
connections.splice(idx, 1);
|
|
path.remove();
|
|
document.getElementById("connection-status").textContent = "Verbindung geloescht";
|
|
}
|
|
});
|
|
|
|
console.log("Connection created:", fromNode.dataset.nodeId, "->", toNode.dataset.nodeId);
|
|
} catch(err) {
|
|
console.error("Error in createConnection:", err);
|
|
}
|
|
}
|
|
|
|
function calculatePath(fromConn, toConn, canvas) {
|
|
var canvasRect = canvas.getBoundingClientRect();
|
|
var fromRect = fromConn.getBoundingClientRect();
|
|
var toRect = toConn.getBoundingClientRect();
|
|
|
|
console.log("Canvas rect:", canvasRect);
|
|
console.log("From rect:", fromRect);
|
|
console.log("To rect:", toRect);
|
|
|
|
// Get center points relative to canvas
|
|
var x1 = fromRect.left + fromRect.width/2 - canvasRect.left;
|
|
var y1 = fromRect.top + fromRect.height/2 - canvasRect.top;
|
|
var x2 = toRect.left + toRect.width/2 - canvasRect.left;
|
|
var y2 = toRect.top + toRect.height/2 - canvasRect.top;
|
|
|
|
console.log("Points: (" + x1 + "," + y1 + ") -> (" + x2 + "," + y2 + ")");
|
|
|
|
// Create curved path (bezier)
|
|
var ctrlY1 = y1 + Math.abs(y2 - y1) * 0.4;
|
|
var ctrlY2 = y2 - Math.abs(y2 - y1) * 0.4;
|
|
|
|
return "M " + x1 + " " + y1 + " C " + x1 + " " + ctrlY1 + ", " + x2 + " " + ctrlY2 + ", " + x2 + " " + y2;
|
|
}
|
|
|
|
function updateConnections() {
|
|
var canvas = document.getElementById("connection-canvas");
|
|
connections.forEach(function(c) {
|
|
if (c.path && c.fromConn && c.toConn) {
|
|
var pathD = calculatePath(c.fromConn, c.toConn, canvas);
|
|
c.path.setAttribute("d", pathD);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Toggle button
|
|
document.getElementById("connection-editor-toggle").addEventListener("click", function() {
|
|
var container = document.getElementById("connection-editor-container");
|
|
if (container.style.display === "none") {
|
|
container.style.display = "block";
|
|
setTimeout(init, 150);
|
|
} else {
|
|
container.style.display = "none";
|
|
}
|
|
});
|
|
|
|
// Expose for external use
|
|
window.ConnectionEditor = {
|
|
getConnections: function() { return connections; },
|
|
clear: function() {
|
|
connections.forEach(function(c) { if (c.path) c.path.remove(); });
|
|
connections = [];
|
|
},
|
|
refresh: function() { updateConnections(); }
|
|
};
|
|
})();
|
|
</script>';
|
|
|
|
print '<div style="margin-top:10px;">';
|
|
print '<button type="button" class="button" onclick="if(window.jsPlumbInstance) { window.jsPlumbInstance.deleteEveryConnection(); console.log(\'Connections cleared\'); }">Alle Verbindungen löschen</button> ';
|
|
print '<button type="button" class="button" onclick="alert(\'Export-Funktion kommt später\');">Verbindungen exportieren</button>';
|
|
print '</div>';
|
|
|
|
print '</div>'; // #jsplumb-container
|
|
print '</div>'; // .kundenkarte-jsplumb-prototype
|
|
}
|
|
|
|
// Action buttons
|
|
print '<div class="tabsAction">';
|
|
if ($permissiontoadd) {
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=edit&anlage_id='.$anlageId.'">'.$langs->trans('Modify').'</a>';
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=copy&anlage_id='.$anlageId.'">'.$langs->trans('Copy').'</a>';
|
|
}
|
|
if ($permissiontodelete) {
|
|
print '<a class="butActionDelete" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=delete&anlage_id='.$anlageId.'">'.$langs->trans('Delete').'</a>';
|
|
}
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'">'.$langs->trans('Back').'</a>';
|
|
print '</div>';
|
|
|
|
} else {
|
|
// Create/Edit/Copy form
|
|
$isEdit = ($action == 'edit');
|
|
$isCopy = ($action == 'copy');
|
|
$formAction = $isEdit ? 'update' : 'add';
|
|
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="'.$formAction.'">';
|
|
if ($isEdit) {
|
|
print '<input type="hidden" name="anlage_id" value="'.$anlageId.'">';
|
|
}
|
|
if ($isCopy) {
|
|
print '<input type="hidden" name="copy_from" value="'.$anlageId.'">';
|
|
}
|
|
|
|
print '<table class="border centpercent" id="element_form_table">';
|
|
|
|
// Label
|
|
$labelValue = '';
|
|
if ($isEdit) {
|
|
$labelValue = $anlage->label;
|
|
} elseif ($isCopy) {
|
|
$labelValue = $anlage->label.' (Kopie)';
|
|
} else {
|
|
$labelValue = GETPOST('label');
|
|
}
|
|
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Label').'</td>';
|
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($labelValue).'" required></td></tr>';
|
|
|
|
// Type
|
|
print '<tr><td class="fieldrequired">'.$langs->trans('Type').'</td>';
|
|
print '<td><select name="fk_anlage_type" class="flat minwidth200" id="select_type" required>';
|
|
print '<option value="">'.$langs->trans('SelectType').'</option>';
|
|
foreach ($types as $t) {
|
|
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
|
|
print '<option value="'.$t->id.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
|
|
}
|
|
print '</select>';
|
|
if (empty($types)) {
|
|
print '<br><span class="warning">'.$langs->trans('NoTypesDefinedForSystem').'</span>';
|
|
}
|
|
print '</td></tr>';
|
|
|
|
// 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 '<tr><td>'.$langs->trans('SelectParent').'</td>';
|
|
print '<td><select name="fk_parent" class="flat minwidth200">';
|
|
print '<option value="0">('.$langs->trans('Root').')</option>';
|
|
printTreeOptions($tree, $selectedParent, $excludeId);
|
|
print '</select></td></tr>';
|
|
|
|
// Dynamic fields will be inserted here via JavaScript
|
|
print '<tbody id="dynamic_fields"></tbody>';
|
|
|
|
// Notes (always at the end)
|
|
print '<tr class="notes-row"><td>'.$langs->trans('FieldNotes').'</td>';
|
|
$noteValue = ($isEdit || $isCopy) ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : '');
|
|
print '<td><textarea name="note_private" class="flat minwidth300" rows="3">'.htmlspecialchars($noteValue, ENT_QUOTES, 'UTF-8').'</textarea></td></tr>';
|
|
|
|
print '</table>';
|
|
|
|
print '<div class="center" style="margin-top:20px;">';
|
|
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
|
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'">'.$langs->trans('Cancel').'</a>';
|
|
print '</div>';
|
|
|
|
print '</form>';
|
|
}
|
|
|
|
print '</div>';
|
|
|
|
} else {
|
|
// Tree view
|
|
if ($permissiontoadd) {
|
|
print '<div style="margin-bottom:15px;">';
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=create">';
|
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
|
|
// 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 '<div class="kundenkarte-tree" data-system="'.$systemId.'">';
|
|
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap);
|
|
print '</div>';
|
|
} else {
|
|
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
print '</div>';
|
|
|
|
print dol_get_fiche_end();
|
|
|
|
// Tooltip container
|
|
print '<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>';
|
|
|
|
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 '<div class="kundenkarte-tree-node" style="margin-left:'.($level * 20).'px;">';
|
|
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
|
|
|
|
// Toggle
|
|
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>';
|
|
}
|
|
|
|
// Icon with tooltip data
|
|
$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>';
|
|
|
|
// Label with tree info in parentheses + file indicators
|
|
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
|
|
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
|
|
if (!empty($treeInfo)) {
|
|
print ' <span class="kundenkarte-tree-label-info">('.dol_escape_htmltag(implode(', ', $treeInfo)).')</span>';
|
|
}
|
|
// File indicators - directly after parentheses
|
|
if ($node->image_count > 0 || $node->doc_count > 0) {
|
|
print ' <span class="kundenkarte-tree-files">';
|
|
if ($node->image_count > 0) {
|
|
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-images kundenkarte-images-trigger" data-anlage-id="'.$node->id.'" title="'.$node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder').'">';
|
|
print '<i class="fa fa-image"></i>';
|
|
if ($node->image_count > 1) {
|
|
print ' '.$node->image_count;
|
|
}
|
|
print '</a>';
|
|
}
|
|
if ($node->doc_count > 0) {
|
|
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-docs kundenkarte-docs-trigger" data-anlage-id="'.$node->id.'" title="'.$node->doc_count.' '.($node->doc_count == 1 ? 'Dokument' : 'Dokumente').'">';
|
|
print '<i class="fa fa-file-pdf-o"></i>';
|
|
if ($node->doc_count > 1) {
|
|
print ' '.$node->doc_count;
|
|
}
|
|
print '</a>';
|
|
}
|
|
print '</span>';
|
|
}
|
|
print '</span>';
|
|
|
|
// Type badge
|
|
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>';
|
|
}
|
|
|
|
// Actions
|
|
print '<span class="kundenkarte-tree-actions">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&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='.$socid.'&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='.$socid.'&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='.$socid.'&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='.$socid.'&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>';
|
|
|
|
// Children
|
|
if ($hasChildren) {
|
|
print '<div class="kundenkarte-tree-children">';
|
|
printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap);
|
|
print '</div>';
|
|
}
|
|
|
|
print '</div>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 '<option value="'.$node->id.'"'.$sel.'>'.$prefix.dol_escape_htmltag($node->label).'</option>';
|
|
|
|
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 = '<div class="kundenkarte-panel" data-panel-id="'.$panel->id.'">';
|
|
|
|
// Panel header
|
|
$html .= '<div class="kundenkarte-panel-header">';
|
|
$html .= '<span class="kundenkarte-panel-label">'.dol_escape_htmltag($panel->label ?: $langs->trans('PanelLabel')).'</span>';
|
|
$html .= '<span class="kundenkarte-panel-actions">';
|
|
if ($canEdit) {
|
|
$html .= '<a href="#" class="kundenkarte-add-carrier" data-anlage-id="'.$panel->fk_anlage.'" data-panel-id="'.$panel->id.'" title="'.$langs->trans('AddCarrier').'"><i class="fa fa-plus"></i></a> ';
|
|
$html .= '<a href="#" class="kundenkarte-panel-edit" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a> ';
|
|
}
|
|
if ($canDelete) {
|
|
$html .= '<a href="#" class="kundenkarte-panel-delete" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a> ';
|
|
}
|
|
$html .= '</span>';
|
|
$html .= '</div>';
|
|
|
|
// Load and render carriers for this panel
|
|
$panel->fetchCarriers();
|
|
$html .= '<div class="kundenkarte-carriers-list">';
|
|
|
|
if (empty($panel->carriers)) {
|
|
$html .= '<div class="opacitymedium" style="padding:10px;">'.$langs->trans('NoCarriers').'</div>';
|
|
} 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 .= '<div class="kundenkarte-carrier-quickadd" data-carrier-id="'.$lastCarrier->id.'" title="'.$langs->trans('DuplicatePreviousCarrier').'">';
|
|
$html .= '<i class="fa fa-plus"></i>';
|
|
$html .= '</div>';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
|
|
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 = '<div class="kundenkarte-carrier" data-carrier-id="'.$carrier->id.'">';
|
|
|
|
// Header
|
|
$html .= '<div class="kundenkarte-carrier-header">';
|
|
$html .= '<span class="kundenkarte-carrier-label">'.dol_escape_htmltag($carrier->label ?: $langs->trans('CarrierLabel')).'</span>';
|
|
$html .= '<span class="kundenkarte-carrier-info">'.$carrier->getUsedTE().'/'.$carrier->total_te.' TE '.$langs->trans('UsedTE').'</span>';
|
|
$html .= '<span class="kundenkarte-carrier-actions">';
|
|
if ($canEdit) {
|
|
$html .= '<a href="#" class="kundenkarte-carrier-add-equipment" title="'.$langs->trans('AddEquipment').'"><i class="fa fa-plus"></i></a>';
|
|
$html .= '<a href="#" class="kundenkarte-carrier-edit" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
|
|
}
|
|
if ($canDelete) {
|
|
$html .= '<a href="#" class="kundenkarte-carrier-delete" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a>';
|
|
}
|
|
$html .= '</span>';
|
|
$html .= '</div>';
|
|
|
|
// SVG Rail
|
|
$html .= '<div class="kundenkarte-carrier-svg-container">';
|
|
$html .= '<svg class="kundenkarte-carrier-svg" width="'.$totalWidth.'" height="'.$svgHeight.'" viewBox="0 0 '.$totalWidth.' '.$svgHeight.'">';
|
|
|
|
// 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 .= '<path d="M'.$bracketX1.' '.$bracketBottomY.' L'.$bracketX1.' '.$bracketTopY.' L'.$bracketX2.' '.$bracketTopY.' L'.$bracketX2.' '.$bracketBottomY.'" ';
|
|
$html .= 'fill="none" stroke="#e74c3c" stroke-width="2" stroke-linecap="round"/>';
|
|
|
|
// Draw label if exists
|
|
if (!empty($group['label'])) {
|
|
$labelX = $bracketX1 + $bracketWidth / 2;
|
|
$html .= '<text x="'.$labelX.'" y="'.($bracketBottomY - 3).'" text-anchor="middle" ';
|
|
$html .= 'fill="#e74c3c" font-size="11px" font-weight="bold">';
|
|
$html .= dol_escape_htmltag($group['label']);
|
|
$html .= '</text>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Background grid (TE markers)
|
|
for ($i = 0; $i <= $carrier->total_te; $i++) {
|
|
$x = $PADDING_LEFT + $i * $TE_WIDTH;
|
|
$html .= '<line x1="'.$x.'" y1="'.$blockYOffset.'" x2="'.$x.'" y2="'.($blockYOffset + $BLOCK_HEIGHT).'" stroke="#555" stroke-width="0.5"/>';
|
|
}
|
|
|
|
// 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 .= '<g class="kundenkarte-equipment-block" data-equipment-id="'.$eq->id.'" ';
|
|
$html .= 'data-tooltip=\''.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'\' ';
|
|
$html .= 'draggable="true" style="cursor:pointer;">';
|
|
|
|
// Block rectangle with rounded corners - more padding between blocks
|
|
$html .= '<rect x="'.($x + 4).'" y="'.($blockYOffset + 5).'" width="'.($width - 8).'" height="'.($BLOCK_HEIGHT - 10).'" ';
|
|
$html .= 'rx="5" ry="5" fill="'.$color.'" stroke="#333" stroke-width="1"/>';
|
|
|
|
// Label text (centered)
|
|
$fontSize = $width < 50 ? 12 : 15;
|
|
$html .= '<text x="'.($x + $width/2).'" y="'.($blockYOffset + $BLOCK_HEIGHT/2).'" ';
|
|
$html .= 'text-anchor="middle" dominant-baseline="middle" ';
|
|
$html .= 'fill="#fff" font-size="'.$fontSize.'px" font-weight="bold">';
|
|
$html .= dol_escape_htmltag($label);
|
|
$html .= '</text>';
|
|
|
|
$html .= '</g>';
|
|
}
|
|
}
|
|
|
|
// 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 .= '<g class="kundenkarte-rail" data-connection-id="'.$rail->id.'" style="cursor:pointer;">';
|
|
|
|
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 .= '<line x1="'.$segX1.'" y1="'.$lineY.'" x2="'.$segX2.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="2" stroke-linecap="round"/>';
|
|
}
|
|
} else {
|
|
// L1, L2, L3 are continuous (no gaps)
|
|
$html .= '<line x1="'.$x1.'" y1="'.$lineY.'" x2="'.$x2.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="2" stroke-linecap="round"/>';
|
|
}
|
|
|
|
// Vertical end caps
|
|
$html .= '<line x1="'.$x1.'" y1="'.$bracketY1.'" x2="'.$x1.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="2"/>';
|
|
$html .= '<line x1="'.$x2.'" y1="'.$bracketY1.'" x2="'.$x2.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="2"/>';
|
|
}
|
|
|
|
// 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 .= '<line x1="'.$tapX.'" y1="'.$bracketY1.'" x2="'.$tapX.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="1.5"/>';
|
|
// Connection dot
|
|
$html .= '<circle cx="'.$tapX.'" cy="'.$lineY.'" r="2" fill="'.$phase['color'].'"/>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} 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 .= '<path d="M'.$x1.' '.$bracketY1.' L'.$x1.' '.$bracketY2.' L'.$x2.' '.$bracketY2.' L'.$x2.' '.$bracketY1.'" ';
|
|
$html .= 'fill="none" stroke="'.$color.'" stroke-width="'.$strokeWidth.'" stroke-linecap="round"/>';
|
|
} 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 .= '<line x1="'.$x1.'" y1="'.$bracketY1.'" x2="'.$x1.'" y2="'.$bracketY2.'" ';
|
|
$html .= 'stroke="'.$color.'" stroke-width="'.$strokeWidth.'" stroke-linecap="round"/>';
|
|
$html .= '<line x1="'.$x2.'" y1="'.$bracketY1.'" x2="'.$x2.'" y2="'.$bracketY2.'" ';
|
|
$html .= 'stroke="'.$color.'" stroke-width="'.$strokeWidth.'" stroke-linecap="round"/>';
|
|
|
|
// Draw horizontal segments
|
|
foreach ($segments as $seg) {
|
|
$segX1 = $paddingLeft + ($seg['start'] - 1) * $teWidth + 5;
|
|
$segX2 = $paddingLeft + ($seg['end']) * $teWidth - 5;
|
|
$html .= '<line x1="'.$segX1.'" y1="'.$bracketY2.'" x2="'.$segX2.'" y2="'.$bracketY2.'" ';
|
|
$html .= 'stroke="'.$color.'" stroke-width="'.$strokeWidth.'" stroke-linecap="round"/>';
|
|
}
|
|
}
|
|
|
|
// 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 .= '<line x1="'.$eqCenter.'" y1="'.$bracketY1.'" x2="'.$eqCenter.'" y2="'.$bracketY2.'" ';
|
|
$html .= 'stroke="'.$color.'" stroke-width="2"/>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 .= '<text x="'.$labelX.'" y="'.$labelY.'" text-anchor="middle" font-size="12" fill="'.$labelColor.'" font-weight="bold">';
|
|
$html .= dol_escape_htmltag($labelText);
|
|
$html .= '</text>';
|
|
}
|
|
|
|
$html .= '</g>';
|
|
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 .= '<g class="kundenkarte-output" data-connection-id="'.$out->id.'" style="cursor:pointer;">';
|
|
|
|
// 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 .= '<text x="'.$eqCenter.'" y="'.($outputY + 18).'" text-anchor="middle" font-size="13" fill="#ddd" font-weight="bold">';
|
|
$html .= dol_escape_htmltag($displayLabel);
|
|
$html .= '</text>';
|
|
|
|
// Medium info below label
|
|
if ($mediumInfo) {
|
|
$html .= '<text x="'.$eqCenter.'" y="'.($outputY + 36).'" text-anchor="middle" font-size="11" fill="#999">';
|
|
$html .= dol_escape_htmltag($mediumInfo);
|
|
$html .= '</text>';
|
|
}
|
|
}
|
|
|
|
// Vertical line from label DOWN to equipment block
|
|
$lineStartY = $outputY + $OUTPUT_HEIGHT - 8;
|
|
$lineEndY = $blockYOffset;
|
|
$html .= '<line x1="'.$eqCenter.'" y1="'.$lineStartY.'" x2="'.$eqCenter.'" y2="'.$lineEndY.'" ';
|
|
$html .= 'stroke="'.$color.'" stroke-width="2" stroke-dasharray="4,2"/>';
|
|
|
|
$html .= '</g>';
|
|
}
|
|
}
|
|
|
|
// 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 .= '</svg>';
|
|
|
|
// Clickable slots overlay (for adding equipment)
|
|
if ($canEdit) {
|
|
$html .= '<div class="kundenkarte-carrier-slots" style="width:'.$totalWidth.'px;height:'.$svgHeight.'px;">';
|
|
|
|
// 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 .= '<div class="kundenkarte-slot-quickadd" data-position="'.$pos.'" data-equipment-id="'.$lastEquipment->id.'" ';
|
|
$html .= 'style="left:'.$slotLeft.'px;width:'.($lastEquipment->width_te * $TE_WIDTH).'px;" ';
|
|
$html .= 'title="'.$langs->trans('DuplicatePrevious').'">';
|
|
$html .= '<i class="fa fa-plus"></i>';
|
|
$html .= '</div>';
|
|
// Skip the slots that would be covered by this button
|
|
$pos += $lastEquipment->width_te - 1;
|
|
} else {
|
|
// Regular empty slot
|
|
$html .= '<div class="kundenkarte-slot-empty" data-position="'.$pos.'" style="left:'.$slotLeft.'px;width:'.$TE_WIDTH.'px;"></div>';
|
|
}
|
|
}
|
|
}
|
|
$html .= '</div>';
|
|
}
|
|
|
|
$html .= '</div>'; // svg-container
|
|
|
|
// Connection Editor Wrapper
|
|
$html .= '<div class="kundenkarte-connection-editor-wrapper">';
|
|
|
|
// Toggle button and action buttons
|
|
$html .= '<div class="kundenkarte-connection-editor-header">';
|
|
$html .= '<a href="#" class="kundenkarte-connection-editor-toggle">';
|
|
$html .= '<i class="fa fa-chevron-down"></i> '.$langs->trans('ConnectionEditor');
|
|
$html .= '</a>';
|
|
|
|
if ($canEdit) {
|
|
$html .= '<div class="kundenkarte-connection-editor-actions">';
|
|
$html .= '<button type="button" class="kundenkarte-add-busbar-btn" data-carrier-id="'.$carrier->id.'" title="'.$langs->trans('AddBusbar').'">';
|
|
$html .= '<i class="fa fa-minus"></i> '.$langs->trans('Busbar');
|
|
$html .= '</button>';
|
|
$html .= '<button type="button" class="kundenkarte-add-conn-btn" data-carrier-id="'.$carrier->id.'" title="'.$langs->trans('AddConnection').'">';
|
|
$html .= '<i class="fa fa-link"></i> '.$langs->trans('Connection');
|
|
$html .= '</button>';
|
|
$html .= '</div>';
|
|
}
|
|
$html .= '</div>'; // header
|
|
|
|
// Editor canvas area (initially hidden)
|
|
$totalWidth = $carrier->total_te * $TE_WIDTH;
|
|
$html .= '<div class="kundenkarte-connection-editor" data-carrier-id="'.$carrier->id.'" data-total-te="'.$carrier->total_te.'">';
|
|
$html .= '<svg class="kundenkarte-connection-svg" width="'.$totalWidth.'" height="120">';
|
|
$html .= '</svg>';
|
|
$html .= '</div>'; // editor
|
|
|
|
$html .= '</div>'; // editor-wrapper
|
|
|
|
$html .= '</div>'; // 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 = '<div class="kundenkarte-connections-container" data-carrier-id="'.$carrier->id.'">';
|
|
$html .= '<svg class="kundenkarte-connections-svg" width="'.$totalWidth.'" height="'.$svgHeight.'" viewBox="0 0 '.$totalWidth.' '.$svgHeight.'">';
|
|
|
|
// 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 .= '<g class="kundenkarte-output" data-connection-id="'.$out->id.'" style="cursor:pointer;">';
|
|
|
|
// Small vertical line down from equipment
|
|
$html .= '<line class="kundenkarte-output-line" x1="'.$eqCenter.'" y1="0" x2="'.$eqCenter.'" y2="'.($outputY + 8).'" stroke="'.$color.'" stroke-width="2" stroke-dasharray="3,2"/>';
|
|
|
|
// Label
|
|
$displayLabel = $out->getDisplayLabel();
|
|
if ($displayLabel) {
|
|
$html .= '<text class="kundenkarte-output-label" x="'.($eqCenter + 3).'" y="'.($outputY + 12).'" font-size="11" fill="#aaa">';
|
|
$html .= dol_escape_htmltag($displayLabel);
|
|
$html .= '</text>';
|
|
}
|
|
|
|
$html .= '</g>';
|
|
}
|
|
|
|
// 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 .= '<g class="kundenkarte-rail" data-connection-id="'.$conn->id.'" style="cursor:pointer;">';
|
|
|
|
// 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 .= '<line x1="'.$segX1.'" y1="'.$lineY.'" x2="'.$segX2.'" y2="'.$lineY.'" ';
|
|
$html .= 'stroke="'.$phase['color'].'" stroke-width="2" stroke-linecap="round"/>';
|
|
}
|
|
}
|
|
} 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 .= '<line class="kundenkarte-rail-line" ';
|
|
$html .= 'x1="'.$segX1.'" y1="'.$railY.'" x2="'.$segX2.'" y2="'.$railY.'" stroke="'.$color.'" stroke-width="4" stroke-linecap="round"/>';
|
|
}
|
|
}
|
|
|
|
// 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 .= '<line x1="'.$eqCenter.'" y1="'.($railY - 6).'" x2="'.$eqCenter.'" y2="'.($railY + 6).'" ';
|
|
$html .= 'stroke="'.$tapColor.'" stroke-width="1.5"/>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Label for rail type
|
|
$labelText = $conn->connection_type;
|
|
if ($railPhases) {
|
|
$labelText = $railPhases;
|
|
}
|
|
if ($labelText) {
|
|
$labelX = ($x1 + $x2) / 2;
|
|
$labelColor = !empty($phaseLines) ? '#555' : $color;
|
|
$html .= '<text x="'.$labelX.'" y="'.($railY - 10).'" text-anchor="middle" font-size="10" fill="'.$labelColor.'" font-weight="bold">';
|
|
$html .= dol_escape_htmltag($labelText);
|
|
$html .= '</text>';
|
|
}
|
|
|
|
$html .= '</g>';
|
|
}
|
|
|
|
// 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 .= '<g class="kundenkarte-connection" data-connection-id="'.$conn->id.'" style="cursor:pointer;">';
|
|
$html .= '<path class="kundenkarte-connection-line" ';
|
|
$html .= 'd="M'.$x1.' 0 L'.$x1.' '.$y.' L'.$x2.' '.$y.' L'.$x2.' 0" stroke="'.$color.'" fill="none" stroke-width="2"/>';
|
|
$html .= '</g>';
|
|
}
|
|
}
|
|
}
|
|
|
|
$html .= '</svg>';
|
|
|
|
// Add connection buttons
|
|
if ($canEdit) {
|
|
$html .= '<div style="display:flex;gap:10px;flex-wrap:wrap;">';
|
|
$html .= '<a href="#" class="kundenkarte-add-connection kundenkarte-add-output-btn" data-carrier-id="'.$carrier->id.'">';
|
|
$html .= '<i class="fa fa-arrow-down"></i> '.$langs->trans('AddOutput');
|
|
$html .= '</a>';
|
|
$html .= '<a href="#" class="kundenkarte-add-connection kundenkarte-add-rail-btn" data-carrier-id="'.$carrier->id.'">';
|
|
$html .= '<i class="fa fa-minus"></i> '.$langs->trans('AddRail');
|
|
$html .= '</a>';
|
|
$html .= '</div>';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|