Neuer Editor

This commit is contained in:
Eduard Wisch 2026-02-11 15:41:33 +01:00
parent 3de514308b
commit 6be50395e5
14 changed files with 4467 additions and 800 deletions

View file

@ -374,7 +374,7 @@ if (in_array($action, array('create', 'edit'))) {
}
print '</select></td></tr>';
// Icon
// FontAwesome Icon (fallback)
print '<tr><td>'.$langs->trans('SystemPicto').'</td>';
print '<td><div class="kundenkarte-icon-picker-wrapper">';
print '<span class="kundenkarte-icon-preview">';
@ -384,7 +384,39 @@ if (in_array($action, array('create', 'edit'))) {
print '</span>';
print '<input type="text" name="picto" class="flat minwidth200" value="'.dol_escape_htmltag($equipmentType->picto).'" placeholder="fa-bolt">';
print '<button type="button" class="kundenkarte-icon-picker-btn" data-input="picto"><i class="fa fa-th"></i> '.$langs->trans('SelectIcon').'</button>';
print '</div></td></tr>';
print '</div>';
print '<span class="opacitymedium small">Fallback-Icon wenn kein Schaltzeichen hochgeladen</span>';
print '</td></tr>';
// Schaltzeichen Upload (SVG/PNG)
print '<tr><td>'.$langs->trans('SchematicSymbol').'</td>';
print '<td>';
print '<div id="icon-upload-area" style="display:flex;align-items:center;gap:15px;">';
// Preview area
print '<div id="icon-preview" style="width:80px;height:80px;border:2px dashed #ccc;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f8f8f8;">';
if ($action == 'edit' && $equipmentType->icon_file) {
$iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($equipmentType->icon_file);
print '<img src="'.$iconUrl.'" style="max-width:70px;max-height:70px;" alt="Icon">';
} else {
print '<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Symbol</span>';
}
print '</div>';
// Upload controls
print '<div>';
print '<input type="file" id="icon-file-input" accept=".svg,.png" style="display:none;">';
print '<button type="button" id="icon-upload-btn" class="button" onclick="document.getElementById(\'icon-file-input\').click();">';
print '<i class="fa fa-upload"></i> SVG/PNG hochladen</button>';
if ($action == 'edit' && $equipmentType->icon_file) {
print ' <button type="button" id="icon-delete-btn" class="button" style="background:#e74c3c;border-color:#c0392b;color:#fff;">';
print '<i class="fa fa-trash"></i> Löschen</button>';
}
print '<div class="opacitymedium small" style="margin-top:5px;">Normgerechte Symbole nach IEC 60617 / DIN EN 60617</div>';
print '</div>';
print '</div>';
print '</td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td>';
@ -410,7 +442,8 @@ if (in_array($action, array('create', 'edit'))) {
print '</table>';
// JavaScript for terminal presets
// JavaScript for terminal presets and icon upload
$typeIdJs = $action == 'edit' ? $typeId : 0;
print '<script>
function setTerminals(type) {
var configs = {
@ -423,6 +456,80 @@ if (in_array($action, array('create', 'edit'))) {
document.getElementById("terminals_config").value = JSON.stringify(configs[type], null, 2);
}
}
// Icon upload handling
var typeId = '.$typeIdJs.';
document.getElementById("icon-file-input").addEventListener("change", function(e) {
if (!typeId || typeId == 0) {
alert("Bitte speichern Sie zuerst den Equipment-Typ bevor Sie ein Symbol hochladen.");
return;
}
var file = e.target.files[0];
if (!file) return;
var formData = new FormData();
formData.append("action", "upload");
formData.append("type_id", typeId);
formData.append("icon_file", file);
formData.append("token", "'.newToken().'");
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_icon.php", {
method: "POST",
body: formData
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var preview = document.getElementById("icon-preview");
preview.innerHTML = \'<img src="\' + data.icon_url + \'?t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Icon">\';
// Add delete button if not present
if (!document.getElementById("icon-delete-btn")) {
var btn = document.createElement("button");
btn.type = "button";
btn.id = "icon-delete-btn";
btn.className = "button";
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
btn.onclick = deleteIcon;
document.getElementById("icon-upload-btn").after(btn);
}
} else {
alert("Fehler: " + data.error);
}
})
.catch(function(err) {
alert("Upload fehlgeschlagen: " + err);
});
e.target.value = "";
});
function deleteIcon() {
if (!confirm("Symbol wirklich löschen?")) return;
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_icon.php", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: "action=delete&type_id=" + typeId + "&token='.newToken().'"
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var preview = document.getElementById("icon-preview");
preview.innerHTML = \'<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Symbol</span>\';
var delBtn = document.getElementById("icon-delete-btn");
if (delBtn) delBtn.remove();
} else {
alert("Fehler: " + data.error);
}
});
}
var delBtn = document.getElementById("icon-delete-btn");
if (delBtn) delBtn.onclick = deleteIcon;
</script>';
print '<div class="center" style="margin-top:20px;">';

View file

@ -66,6 +66,9 @@ switch ($action) {
'fk_carrier' => $equipment->fk_carrier,
'type_id' => $equipment->fk_equipment_type,
'type_label' => $equipment->type_label,
'type_label_short' => $equipment->type_label_short,
'type_color' => $equipment->type_color,
'type_icon_file' => $equipment->type_icon_file,
'label' => $equipment->label,
'position_te' => $equipment->position_te,
'width_te' => $equipment->width_te,
@ -107,6 +110,10 @@ switch ($action) {
$items = $equipment->fetchByCarrier($carrierId);
$result = array();
foreach ($items as $eq) {
$iconUrl = '';
if (!empty($eq->type_icon_file)) {
$iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($eq->type_icon_file);
}
$result[] = array(
'id' => $eq->id,
'type_id' => $eq->fk_equipment_type,
@ -114,6 +121,8 @@ switch ($action) {
'type_label_short' => $eq->type_label_short,
'type_ref' => $eq->type_ref,
'type_color' => $eq->type_color,
'type_icon_file' => $eq->type_icon_file,
'type_icon_url' => $iconUrl,
'terminals_config' => $eq->terminals_config,
'label' => $eq->label,
'position_te' => $eq->position_te,
@ -301,14 +310,21 @@ switch ($action) {
if ($newEquipment->fetch($newId) > 0) {
$response['equipment'] = array(
'id' => $newEquipment->id,
'fk_carrier' => $newEquipment->fk_carrier,
'type_id' => $newEquipment->fk_equipment_type,
'type_label' => $newEquipment->type_label,
'type_label_short' => $newEquipment->type_label_short,
'type_color' => $newEquipment->type_color,
'type_ref' => $newEquipment->type_ref,
'type_icon_file' => $newEquipment->type_icon_file,
'terminals_config' => $newEquipment->terminals_config,
'label' => $newEquipment->label,
'position_te' => $newEquipment->position_te,
'width_te' => $newEquipment->width_te,
'block_label' => $newEquipment->getBlockLabel(),
'block_color' => $newEquipment->getBlockColor()
'block_color' => $newEquipment->getBlockColor(),
'field_values' => $newEquipment->getFieldValues(),
'fk_product' => $newEquipment->fk_product
);
}
} else {

View file

@ -153,6 +153,7 @@ switch ($action) {
$connection->rail_end_te = GETPOSTINT('rail_end_te');
$connection->fk_carrier = $carrierId;
$connection->position_y = GETPOSTINT('position_y');
$connection->path_data = GETPOST('path_data', 'nohtml');
$result = $connection->create($user);
if ($result > 0) {
@ -169,22 +170,24 @@ switch ($action) {
break;
}
if ($connection->fetch($connectionId) > 0) {
$connection->fk_source = GETPOSTINT('fk_source');
$connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output';
$connection->fk_target = GETPOSTINT('fk_target');
$connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: 'input';
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
$connection->color = GETPOST('color', 'alphanohtml');
$connection->output_label = GETPOST('output_label', 'alphanohtml');
$connection->medium_type = GETPOST('medium_type', 'alphanohtml');
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
$connection->is_rail = GETPOSTINT('is_rail');
$connection->rail_start_te = GETPOSTINT('rail_start_te');
$connection->rail_end_te = GETPOSTINT('rail_end_te');
$connection->rail_phases = GETPOST('rail_phases', 'alphanohtml');
$connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
$connection->position_y = GETPOSTINT('position_y');
// Only update fields that are actually sent (preserve existing values)
if (GETPOSTISSET('fk_source')) $connection->fk_source = GETPOSTINT('fk_source');
if (GETPOSTISSET('source_terminal')) $connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: $connection->source_terminal;
if (GETPOSTISSET('fk_target')) $connection->fk_target = GETPOSTINT('fk_target');
if (GETPOSTISSET('target_terminal')) $connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: $connection->target_terminal;
if (GETPOSTISSET('connection_type')) $connection->connection_type = GETPOST('connection_type', 'alphanohtml');
if (GETPOSTISSET('color')) $connection->color = GETPOST('color', 'alphanohtml');
if (GETPOSTISSET('output_label')) $connection->output_label = GETPOST('output_label', 'alphanohtml');
if (GETPOSTISSET('medium_type')) $connection->medium_type = GETPOST('medium_type', 'alphanohtml');
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
if (GETPOSTISSET('is_rail')) $connection->is_rail = GETPOSTINT('is_rail');
if (GETPOSTISSET('rail_start_te')) $connection->rail_start_te = GETPOSTINT('rail_start_te');
if (GETPOSTISSET('rail_end_te')) $connection->rail_end_te = GETPOSTINT('rail_end_te');
if (GETPOSTISSET('rail_phases')) $connection->rail_phases = GETPOST('rail_phases', 'alphanohtml');
if (GETPOSTISSET('excluded_te')) $connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
if (GETPOSTISSET('position_y')) $connection->position_y = GETPOSTINT('position_y');
if (GETPOSTISSET('path_data')) $connection->path_data = GETPOST('path_data', 'nohtml');
$result = $connection->update($user);
if ($result > 0) {
@ -276,30 +279,59 @@ switch ($action) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
$carrierObj = new EquipmentCarrier($db);
$carriers = $carrierObj->fetchByAnlage($anlageId);
$carrierIds = array();
foreach ($carriers as $carrier) {
$carrierIds[] = (int)$carrier->id;
}
$allConnections = array();
foreach ($carriers as $carrier) {
$connections = $connection->fetchByCarrier($carrier->id);
foreach ($connections as $c) {
$allConnections[] = array(
'id' => $c->id,
'fk_source' => $c->fk_source,
'source_terminal' => $c->source_terminal,
'source_terminal_id' => $c->source_terminal_id,
'source_label' => $c->source_label,
'fk_target' => $c->fk_target,
'target_terminal' => $c->target_terminal,
'target_terminal_id' => $c->target_terminal_id,
'target_label' => $c->target_label,
'connection_type' => $c->connection_type,
'color' => $c->getColor(),
'output_label' => $c->output_label,
'medium_type' => $c->medium_type,
'medium_spec' => $c->medium_spec,
'medium_length' => $c->medium_length,
'is_rail' => $c->is_rail,
'fk_carrier' => $c->fk_carrier
);
if (!empty($carrierIds)) {
// Find all connections where source OR target equipment belongs to this anlage's carriers
// This includes connections with fk_carrier=NULL
$sql = "SELECT DISTINCT c.*,
se.label as source_label, se.position_te as source_pos, se.width_te as source_width,
te.label as target_label, te.position_te as target_pos
FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection c
LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment se ON c.fk_source = se.rowid
LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment te ON c.fk_target = te.rowid
WHERE (c.fk_carrier IN (".implode(',', $carrierIds).")
OR se.fk_carrier IN (".implode(',', $carrierIds).")
OR te.fk_carrier IN (".implode(',', $carrierIds)."))
AND c.status = 1";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$allConnections[] = array(
'id' => $obj->rowid,
'fk_source' => $obj->fk_source,
'source_terminal' => $obj->source_terminal,
'source_terminal_id' => $obj->source_terminal_id,
'source_label' => $obj->source_label,
'source_pos' => $obj->source_pos,
'source_width' => $obj->source_width,
'fk_target' => $obj->fk_target,
'target_terminal' => $obj->target_terminal,
'target_terminal_id' => $obj->target_terminal_id,
'target_label' => $obj->target_label,
'target_pos' => $obj->target_pos,
'connection_type' => $obj->connection_type,
'color' => $obj->color ?: '#3498db',
'output_label' => $obj->output_label,
'medium_type' => $obj->medium_type,
'medium_spec' => $obj->medium_spec,
'medium_length' => $obj->medium_length,
'is_rail' => $obj->is_rail,
'rail_start_te' => $obj->rail_start_te,
'rail_end_te' => $obj->rail_end_te,
'rail_phases' => $obj->rail_phases,
'position_y' => $obj->position_y,
'fk_carrier' => $obj->fk_carrier,
'path_data' => isset($obj->path_data) ? $obj->path_data : null
);
}
$db->free($resql);
}
}

View file

@ -0,0 +1,148 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX handler for equipment type icon upload
* Accepts SVG and PNG files for schematic symbols
*/
$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/files.lib.php';
dol_include_once('/kundenkarte/class/equipmenttype.class.php');
header('Content-Type: application/json');
// Security check
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
echo json_encode(array('success' => false, 'error' => 'Access denied'));
exit;
}
$action = GETPOST('action', 'aZ09');
$typeId = GETPOSTINT('type_id');
$response = array('success' => false);
// Directory for equipment type icons
$uploadDir = DOL_DATA_ROOT.'/kundenkarte/equipment_icons/';
// Create directory if not exists
if (!is_dir($uploadDir)) {
dol_mkdir($uploadDir);
}
switch ($action) {
case 'upload':
if (empty($_FILES['icon_file']) || $_FILES['icon_file']['error'] !== UPLOAD_ERR_OK) {
$response['error'] = 'No file uploaded or upload error';
break;
}
$file = $_FILES['icon_file'];
$fileName = dol_sanitizeFileName($file['name']);
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// Validate file type
$allowedExtensions = array('svg', 'png');
if (!in_array($fileExt, $allowedExtensions)) {
$response['error'] = 'Invalid file type. Only SVG and PNG are allowed.';
break;
}
// Validate MIME type
$mimeType = mime_content_type($file['tmp_name']);
$allowedMimes = array('image/svg+xml', 'image/png', 'text/plain', 'text/xml', 'application/xml');
if (!in_array($mimeType, $allowedMimes)) {
$response['error'] = 'Invalid MIME type: '.$mimeType;
break;
}
// For SVG files, do basic security check
if ($fileExt === 'svg') {
$content = file_get_contents($file['tmp_name']);
// Check for potentially dangerous content
$dangerous = array('<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=');
foreach ($dangerous as $pattern) {
if (stripos($content, $pattern) !== false) {
$response['error'] = 'SVG contains potentially dangerous content';
break 2;
}
}
}
// Generate unique filename
$newFileName = 'icon_'.$typeId.'_'.dol_now().'.'.$fileExt;
$destPath = $uploadDir.$newFileName;
// Move uploaded file
if (move_uploaded_file($file['tmp_name'], $destPath)) {
// Update database
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
// Delete old icon file if exists
if ($equipmentType->icon_file && file_exists($uploadDir.$equipmentType->icon_file)) {
unlink($uploadDir.$equipmentType->icon_file);
}
$equipmentType->icon_file = $newFileName;
$result = $equipmentType->update($user);
if ($result > 0) {
$response['success'] = true;
$response['icon_file'] = $newFileName;
$response['icon_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.$newFileName;
} else {
$response['error'] = 'Database update failed';
// Remove uploaded file on DB error
unlink($destPath);
}
} else {
$response['error'] = 'Equipment type not found';
unlink($destPath);
}
} else {
$response['error'] = 'Failed to move uploaded file';
}
break;
case 'delete':
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
if ($equipmentType->icon_file && file_exists($uploadDir.$equipmentType->icon_file)) {
unlink($uploadDir.$equipmentType->icon_file);
}
$equipmentType->icon_file = '';
$result = $equipmentType->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Database update failed';
}
} else {
$response['error'] = 'Equipment type not found';
}
break;
case 'get':
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
$response['success'] = true;
$response['icon_file'] = $equipmentType->icon_file;
if ($equipmentType->icon_file) {
$response['icon_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.$equipmentType->icon_file;
}
} else {
$response['error'] = 'Equipment type not found';
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);
$db->close();

View file

@ -0,0 +1,717 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* PDF Export for Schematic Editor (Leitungslaufplan)
* Following DIN EN 61082 (Document structure) and DIN EN 81346 (Reference designation)
*/
$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/pdf.lib.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/equipment.class.php');
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
dol_include_once('/kundenkarte/class/equipmentconnection.class.php');
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
// Get parameters
$anlageId = GETPOSTINT('anlage_id');
$svgContent = GETPOST('svg_content', 'restricthtml');
$format = GETPOST('format', 'alpha') ?: 'A4';
$orientation = GETPOST('orientation', 'alpha') ?: 'L'; // L=Landscape, P=Portrait
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
accessforbidden();
}
// Load Anlage data
$anlage = new Anlage($db);
if ($anlage->fetch($anlageId) <= 0) {
die('Anlage not found');
}
// Load company
$societe = new Societe($db);
$societe->fetch($anlage->fk_soc);
// Load carriers for this anlage
$carrier = new EquipmentCarrier($db);
$carriers = $carrier->fetchByAnlage($anlageId);
// Load equipment
$equipment = new Equipment($db);
$equipmentList = array();
foreach ($carriers as $c) {
$eqList = $equipment->fetchByCarrier($c->id);
$equipmentList = array_merge($equipmentList, $eqList);
}
// Load connections
$connection = new EquipmentConnection($db);
$connections = array();
foreach ($carriers as $c) {
$connList = $connection->fetchByCarrier($c->id);
$connections = array_merge($connections, $connList);
}
// Create PDF - Landscape A3 or A4 for schematic
$pdf = pdf_getInstance();
$pdf->SetCreator('Dolibarr - Kundenkarte Schaltplan');
$pdf->SetAuthor($user->getFullName($langs));
$pdf->SetTitle('Leitungslaufplan - '.$anlage->label);
// Page format
if ($format == 'A3') {
$pageWidth = 420;
$pageHeight = 297;
} else {
$pageWidth = 297;
$pageHeight = 210;
}
if ($orientation == 'P') {
$tmp = $pageWidth;
$pageWidth = $pageHeight;
$pageHeight = $tmp;
}
$pdf->SetMargins(10, 10, 10);
$pdf->SetAutoPageBreak(false);
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
// ============================================
// DIN EN 61082 / ISO 7200 Title Block (Schriftfeld)
// Position: Bottom right corner
// ============================================
$titleBlockWidth = 180;
$titleBlockHeight = 56;
$titleBlockX = $pageWidth - $titleBlockWidth - 10;
$titleBlockY = $pageHeight - $titleBlockHeight - 10;
// Draw title block frame
$pdf->SetDrawColor(0, 0, 0);
$pdf->SetLineWidth(0.5);
$pdf->Rect($titleBlockX, $titleBlockY, $titleBlockWidth, $titleBlockHeight);
// Title block grid - following DIN structure
// Row heights from bottom: 8, 8, 8, 8, 8, 8, 8 = 56mm total
$rowHeight = 8;
$rows = 7;
// Column widths: 30 | 50 | 50 | 50 = 180mm
$col1 = 30; // Labels
$col2 = 50; // Company info
$col3 = 50; // Document info
$col4 = 50; // Revision info
// Draw horizontal lines
for ($i = 1; $i < $rows; $i++) {
$y = $titleBlockY + ($i * $rowHeight);
$pdf->Line($titleBlockX, $y, $titleBlockX + $titleBlockWidth, $y);
}
// Draw vertical lines
$pdf->Line($titleBlockX + $col1, $titleBlockY, $titleBlockX + $col1, $titleBlockY + $titleBlockHeight);
$pdf->Line($titleBlockX + $col1 + $col2, $titleBlockY, $titleBlockX + $col1 + $col2, $titleBlockY + $titleBlockHeight);
$pdf->Line($titleBlockX + $col1 + $col2 + $col3, $titleBlockY, $titleBlockX + $col1 + $col2 + $col3, $titleBlockY + $titleBlockHeight);
// Fill in title block content
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetTextColor(0, 0, 0);
// Row 1 (from top): Document title spanning full width
$pdf->SetFont('dejavusans', 'B', 12);
$pdf->SetXY($titleBlockX + 2, $titleBlockY + 1);
$pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, 'LEITUNGSLAUFPLAN', 0, 0, 'C');
// Row 2: Installation name
$pdf->SetFont('dejavusans', 'B', 10);
$pdf->SetXY($titleBlockX + 2, $titleBlockY + $rowHeight + 1);
$pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, $anlage->label, 0, 0, 'C');
// Row 3: Labels
$pdf->SetFont('dejavusans', '', 6);
$y = $titleBlockY + (2 * $rowHeight);
$pdf->SetXY($titleBlockX + 1, $y + 1);
$pdf->Cell($col1 - 2, 3, 'Erstellt', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
$pdf->Cell($col2 - 2, 3, 'Kunde', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
$pdf->Cell($col3 - 2, 3, 'Projekt-Nr.', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
$pdf->Cell($col4 - 2, 3, 'Blatt', 0, 0, 'L');
// Row 3: Values
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetXY($titleBlockX + 1, $y + 4);
$pdf->Cell($col1 - 2, 4, dol_print_date(dol_now(), 'day'), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
$pdf->Cell($col2 - 2, 4, dol_trunc($societe->name, 25), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
$pdf->Cell($col3 - 2, 4, $anlage->ref ?: '-', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
$pdf->Cell($col4 - 2, 4, '1 / 1', 0, 0, 'L');
// Row 4: More labels
$y = $titleBlockY + (3 * $rowHeight);
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetXY($titleBlockX + 1, $y + 1);
$pdf->Cell($col1 - 2, 3, 'Bearbeiter', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
$pdf->Cell($col2 - 2, 3, 'Adresse', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
$pdf->Cell($col3 - 2, 3, 'Anlage', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
$pdf->Cell($col4 - 2, 3, 'Revision', 0, 0, 'L');
// Row 4: Values
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetXY($titleBlockX + 1, $y + 4);
$pdf->Cell($col1 - 2, 4, dol_trunc($user->getFullName($langs), 15), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
$address = trim($societe->address.' '.$societe->zip.' '.$societe->town);
$pdf->Cell($col2 - 2, 4, dol_trunc($address, 25), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
$pdf->Cell($col3 - 2, 4, $anlage->type_label ?: '-', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
$pdf->Cell($col4 - 2, 4, 'A', 0, 0, 'L');
// Row 5: Equipment count
$y = $titleBlockY + (4 * $rowHeight);
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetXY($titleBlockX + 1, $y + 1);
$pdf->Cell($col1 - 2, 3, 'Komponenten', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
$pdf->Cell($col2 - 2, 3, 'Verbindungen', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
$pdf->Cell($col3 - 2, 3, 'Hutschienen', 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
$pdf->Cell($col4 - 2, 3, 'Format', 0, 0, 'L');
$pdf->SetFont('dejavusans', '', 8);
$pdf->SetXY($titleBlockX + 1, $y + 4);
$pdf->Cell($col1 - 2, 4, count($equipmentList), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
$pdf->Cell($col2 - 2, 4, count($connections), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
$pdf->Cell($col3 - 2, 4, count($carriers), 0, 0, 'L');
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
$pdf->Cell($col4 - 2, 4, $format.' '.$orientation, 0, 0, 'L');
// Row 6: Norm reference
$y = $titleBlockY + (5 * $rowHeight);
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetXY($titleBlockX + 1, $y + 2);
$pdf->Cell($titleBlockWidth - 2, 4, 'Erstellt nach DIN EN 61082 / DIN EN 81346', 0, 0, 'C');
// Row 7: Company info
$y = $titleBlockY + (6 * $rowHeight);
$pdf->SetFont('dejavusans', 'B', 7);
$pdf->SetXY($titleBlockX + 1, $y + 2);
$pdf->Cell($titleBlockWidth - 2, 4, $mysoc->name, 0, 0, 'C');
// ============================================
// Draw the Schematic Content Area
// ============================================
$schematicX = 10;
$schematicY = 10;
$schematicWidth = $pageWidth - 20;
$schematicHeight = $titleBlockY - 15;
// Draw frame around schematic area
$pdf->SetDrawColor(0, 0, 0);
$pdf->SetLineWidth(0.3);
$pdf->Rect($schematicX, $schematicY, $schematicWidth, $schematicHeight);
// If SVG content provided, embed it
if (!empty($svgContent)) {
// Clean SVG for TCPDF
$svgContent = preg_replace('/<\?xml[^>]*\?>/', '', $svgContent);
$svgContent = preg_replace('/<!DOCTYPE[^>]*>/', '', $svgContent);
// Try to embed SVG
try {
// Scale SVG to fit in schematic area
$pdf->ImageSVG('@'.$svgContent, $schematicX + 2, $schematicY + 2, $schematicWidth - 4, $schematicHeight - 4, '', '', '', 0, false);
} catch (Exception $e) {
// SVG embedding failed - draw placeholder
$pdf->SetFont('dejavusans', 'I', 10);
$pdf->SetXY($schematicX + 10, $schematicY + 10);
$pdf->Cell(0, 10, 'SVG konnte nicht eingebettet werden: '.$e->getMessage(), 0, 1);
}
} else {
// Draw schematic manually if no SVG provided
drawSchematicContent($pdf, $carriers, $equipmentList, $connections, $schematicX, $schematicY, $schematicWidth, $schematicHeight);
}
// ============================================
// Add Wiring List on second page (Verdrahtungsliste)
// ============================================
if (count($connections) > 0) {
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
// Title
$pdf->SetFont('dejavusans', 'B', 14);
$pdf->SetXY(10, 10);
$pdf->Cell(0, 8, 'VERDRAHTUNGSLISTE / KLEMMENPLAN', 0, 1, 'L');
$pdf->SetFont('dejavusans', '', 9);
$pdf->SetXY(10, 18);
$pdf->Cell(0, 5, $anlage->label.' - '.$societe->name, 0, 1, 'L');
// Table header
$pdf->SetY(28);
$pdf->SetFont('dejavusans', 'B', 8);
$pdf->SetFillColor(220, 220, 220);
$colWidths = array(15, 35, 25, 35, 25, 25, 30, 30);
$headers = array('Nr.', 'Von (Quelle)', 'Klemme', 'Nach (Ziel)', 'Klemme', 'Typ', 'Leitung', 'Bemerkung');
$x = 10;
for ($i = 0; $i < count($headers); $i++) {
$pdf->SetXY($x, 28);
$pdf->Cell($colWidths[$i], 6, $headers[$i], 1, 0, 'C', true);
$x += $colWidths[$i];
}
// Table content
$pdf->SetFont('dejavusans', '', 7);
$y = 34;
$lineNum = 1;
// Build equipment lookup
$eqLookup = array();
foreach ($equipmentList as $eq) {
$eqLookup[$eq->id] = $eq;
}
foreach ($connections as $conn) {
// Skip rails/busbars in wiring list (they're separate)
if ($conn->is_rail) continue;
$sourceName = '-';
$sourceTerminal = $conn->source_terminal ?: '-';
$targetName = '-';
$targetTerminal = $conn->target_terminal ?: '-';
if ($conn->fk_source && isset($eqLookup[$conn->fk_source])) {
$sourceName = $eqLookup[$conn->fk_source]->label ?: $eqLookup[$conn->fk_source]->type_label_short;
}
if ($conn->fk_target && isset($eqLookup[$conn->fk_target])) {
$targetName = $eqLookup[$conn->fk_target]->label ?: $eqLookup[$conn->fk_target]->type_label_short;
}
// Connection type / medium
$connType = $conn->connection_type ?: '-';
$medium = trim($conn->medium_type.' '.$conn->medium_spec);
if (empty($medium)) $medium = '-';
$remark = $conn->output_label ?: '';
if ($y > $pageHeight - 25) {
// New page
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
$y = 10;
// Repeat header
$pdf->SetFont('dejavusans', 'B', 8);
$x = 10;
for ($i = 0; $i < count($headers); $i++) {
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[$i], 6, $headers[$i], 1, 0, 'C', true);
$x += $colWidths[$i];
}
$y += 6;
$pdf->SetFont('dejavusans', '', 7);
}
$x = 10;
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[0], 5, $lineNum, 1, 0, 'C');
$x += $colWidths[0];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[1], 5, dol_trunc($sourceName, 18), 1, 0, 'L');
$x += $colWidths[1];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[2], 5, $sourceTerminal, 1, 0, 'C');
$x += $colWidths[2];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[3], 5, dol_trunc($targetName, 18), 1, 0, 'L');
$x += $colWidths[3];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[4], 5, $targetTerminal, 1, 0, 'C');
$x += $colWidths[4];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[5], 5, $connType, 1, 0, 'C');
$x += $colWidths[5];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[6], 5, dol_trunc($medium, 15), 1, 0, 'L');
$x += $colWidths[6];
$pdf->SetXY($x, $y);
$pdf->Cell($colWidths[7], 5, dol_trunc($remark, 15), 1, 0, 'L');
$y += 5;
$lineNum++;
}
// Add busbars section if any
$busbars = array_filter($connections, function($c) { return $c->is_rail; });
if (count($busbars) > 0) {
$y += 10;
if ($y > $pageHeight - 40) {
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
$y = 10;
}
$pdf->SetFont('dejavusans', 'B', 10);
$pdf->SetXY(10, $y);
$pdf->Cell(0, 6, 'SAMMELSCHIENEN / PHASENSCHIENEN', 0, 1, 'L');
$y += 8;
$pdf->SetFont('dejavusans', 'B', 8);
$bbHeaders = array('Nr.', 'Bezeichnung', 'Typ', 'Von TE', 'Bis TE', 'Phasen', 'Ausnahmen');
$bbWidths = array(15, 50, 30, 20, 20, 30, 50);
$x = 10;
for ($i = 0; $i < count($bbHeaders); $i++) {
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[$i], 6, $bbHeaders[$i], 1, 0, 'C', true);
$x += $bbWidths[$i];
}
$y += 6;
$pdf->SetFont('dejavusans', '', 7);
$bbNum = 1;
foreach ($busbars as $bb) {
$x = 10;
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[0], 5, $bbNum, 1, 0, 'C');
$x += $bbWidths[0];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[1], 5, $bb->output_label ?: 'Sammelschiene '.$bbNum, 1, 0, 'L');
$x += $bbWidths[1];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[2], 5, $bb->connection_type ?: '-', 1, 0, 'C');
$x += $bbWidths[2];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[3], 5, $bb->rail_start_te ?: '-', 1, 0, 'C');
$x += $bbWidths[3];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[4], 5, $bb->rail_end_te ?: '-', 1, 0, 'C');
$x += $bbWidths[4];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[5], 5, $bb->rail_phases ?: '-', 1, 0, 'C');
$x += $bbWidths[5];
$pdf->SetXY($x, $y);
$pdf->Cell($bbWidths[6], 5, $bb->excluded_te ?: '-', 1, 0, 'L');
$y += 5;
$bbNum++;
}
}
}
// Output PDF
$filename = 'Leitungslaufplan_'.dol_sanitizeFileName($anlage->label).'_'.date('Y-m-d').'.pdf';
$pdf->Output($filename, 'D');
/**
* Draw schematic content directly in PDF
* Shows only the actual equipment and connections from the database (what was drawn in the editor)
*/
function drawSchematicContent(&$pdf, $carriers, $equipment, $connections, $startX, $startY, $width, $height) {
// Phase colors (DIN VDE compliant)
$phaseColors = array(
'L1' => array(139, 69, 19), // Brown
'L2' => array(0, 0, 0), // Black
'L3' => array(128, 128, 128), // Gray
'N' => array(0, 102, 204), // Blue
'PE' => array(0, 128, 0) // Green (simplified from green-yellow)
);
// Layout constants
$teWidth = 10; // mm per TE
$equipmentStartY = $startY + 20;
$blockWidth = 8;
$blockHeight = 20;
// Calculate total width needed
$maxTE = 0;
foreach ($carriers as $carrier) {
$maxTE = max($maxTE, $carrier->total_te ?: 12);
}
$contentWidth = min($width - 40, $maxTE * $teWidth + 40);
$contentStartX = $startX + 20;
// ========================================
// Draw equipment and connections
// ========================================
$carrierIndex = 0;
foreach ($carriers as $carrier) {
$carrierY = $equipmentStartY + $carrierIndex * 50;
$carrierX = $contentStartX;
$totalTE = $carrier->total_te ?: 12;
// Carrier label
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetTextColor(100, 100, 100);
$pdf->SetXY($carrierX - 15, $carrierY + $blockHeight / 2 - 2);
$pdf->Cell(12, 4, $carrier->label ?: 'H'.($carrierIndex+1), 0, 0, 'R');
// Get equipment on this carrier
$carrierEquipment = array_filter($equipment, function($eq) use ($carrier) {
return $eq->fk_carrier == $carrier->id;
});
// Get busbars for this carrier
$carrierBusbars = array_filter($connections, function($c) use ($carrier) {
return $c->is_rail && $c->fk_carrier == $carrier->id;
});
// Sort equipment by position
usort($carrierEquipment, function($a, $b) {
return ($a->position_te ?: 1) - ($b->position_te ?: 1);
});
// Draw each equipment
foreach ($carrierEquipment as $eq) {
$eqPosTE = $eq->position_te ?: 1;
$eqWidthTE = $eq->width_te ?: 1;
$eqX = $carrierX + ($eqPosTE - 1) * $teWidth;
$eqWidth = $eqWidthTE * $teWidth - 2;
// Equipment block
$color = $eq->type_color ?: '#3498db';
list($r, $g, $b) = sscanf($color, "#%02x%02x%02x");
$pdf->SetFillColor($r ?: 52, $g ?: 152, $b ?: 219);
$pdf->Rect($eqX, $carrierY, $eqWidth, $blockHeight, 'F');
// Equipment label
$pdf->SetFont('dejavusans', 'B', 5);
$pdf->SetTextColor(255, 255, 255);
$label = $eq->type_label_short ?: $eq->label;
$pdf->SetXY($eqX, $carrierY + 3);
$pdf->Cell($eqWidth, 4, dol_trunc($label, 8), 0, 0, 'C');
// Second line label
if ($eq->label && $eq->type_label_short) {
$pdf->SetFont('dejavusans', '', 4);
$pdf->SetXY($eqX, $carrierY + 8);
$pdf->Cell($eqWidth, 3, dol_trunc($eq->label, 10), 0, 0, 'C');
}
$pdf->SetTextColor(0, 0, 0);
// Consumer label below equipment
$pdf->SetFont('dejavusans', '', 5);
$pdf->SetTextColor(80, 80, 80);
$pdf->SetXY($eqX - 2, $carrierY + $blockHeight + 1);
$consumerLabel = $eq->label ?: '';
$pdf->Cell($eqWidth + 4, 4, dol_trunc($consumerLabel, 12), 0, 0, 'C');
}
// Draw busbars (Phasenschienen) for this carrier
foreach ($carrierBusbars as $busbar) {
$busbarStartTE = $busbar->rail_start_te ?: 1;
$busbarEndTE = $busbar->rail_end_te ?: $busbarStartTE;
$busbarX = $carrierX + ($busbarStartTE - 1) * $teWidth;
$busbarWidth = ($busbarEndTE - $busbarStartTE + 1) * $teWidth;
$busbarY = $carrierY - 5;
// Busbar color
$phase = $busbar->rail_phases ?: $busbar->connection_type ?: 'L1';
$color = $phaseColors[$phase] ?? array(200, 100, 50);
$pdf->SetFillColor($color[0], $color[1], $color[2]);
$pdf->Rect($busbarX, $busbarY, $busbarWidth, 3, 'F');
// Draw vertical taps from busbar to equipment
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
for ($te = $busbarStartTE; $te <= $busbarEndTE; $te++) {
// Check if there's equipment at this TE
$hasEquipment = false;
foreach ($carrierEquipment as $eq) {
$eqStart = $eq->position_te ?: 1;
$eqEnd = $eqStart + ($eq->width_te ?: 1) - 1;
if ($te >= $eqStart && $te <= $eqEnd) {
$hasEquipment = true;
break;
}
}
if ($hasEquipment) {
$tapX = $carrierX + ($te - 1) * $teWidth + $teWidth / 2;
$pdf->Line($tapX, $busbarY + 3, $tapX, $carrierY);
}
}
// Busbar label
$pdf->SetFont('dejavusans', 'B', 5);
$pdf->SetTextColor(255, 255, 255);
$pdf->SetXY($busbarX, $busbarY - 0.5);
$pdf->Cell($busbarWidth, 4, $phase, 0, 0, 'C');
}
// ========================================
// Draw Inputs (Anschlusspunkte) and Outputs (Abgänge)
// ========================================
// Get non-rail connections for this carrier's equipment
$carrierEqIds = array_map(function($eq) { return $eq->id; }, $carrierEquipment);
foreach ($connections as $conn) {
if ($conn->is_rail) continue;
// ANSCHLUSSPUNKT (Input) - fk_source is NULL, fk_target exists
if (empty($conn->fk_source) && !empty($conn->fk_target)) {
// Find target equipment
$targetEq = null;
foreach ($carrierEquipment as $eq) {
if ($eq->id == $conn->fk_target) {
$targetEq = $eq;
break;
}
}
if (!$targetEq) continue;
$eqPosTE = $targetEq->position_te ?: 1;
$eqWidthTE = $targetEq->width_te ?: 1;
$eqCenterX = $carrierX + ($eqPosTE - 1) * $teWidth + ($eqWidthTE * $teWidth) / 2;
$lineStartY = $carrierY - 18;
$lineEndY = $carrierY;
// Phase color
$phase = $conn->connection_type ?: 'L1';
$color = $phaseColors[$phase] ?? array(100, 100, 100);
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
$pdf->SetFillColor($color[0], $color[1], $color[2]);
// Vertical line from top
$pdf->Line($eqCenterX, $lineStartY, $eqCenterX, $lineEndY);
// Circle at top (source indicator)
$pdf->Circle($eqCenterX, $lineStartY, 1.5, 0, 360, 'F');
// Phase label
$pdf->SetFont('dejavusans', 'B', 7);
$pdf->SetTextColor($color[0], $color[1], $color[2]);
$pdf->SetXY($eqCenterX - 5, $lineStartY - 5);
$pdf->Cell(10, 4, $phase, 0, 0, 'C');
// Optional label
if (!empty($conn->output_label)) {
$pdf->SetFont('dejavusans', '', 5);
$pdf->SetTextColor(100, 100, 100);
$pdf->SetXY($eqCenterX + 3, $lineStartY - 4);
$pdf->Cell(20, 3, dol_trunc($conn->output_label, 12), 0, 0, 'L');
}
}
// ABGANG (Output) - fk_source exists, fk_target is NULL
if (!empty($conn->fk_source) && empty($conn->fk_target)) {
// Find source equipment
$sourceEq = null;
foreach ($carrierEquipment as $eq) {
if ($eq->id == $conn->fk_source) {
$sourceEq = $eq;
break;
}
}
if (!$sourceEq) continue;
$eqPosTE = $sourceEq->position_te ?: 1;
$eqWidthTE = $sourceEq->width_te ?: 1;
$eqCenterX = $carrierX + ($eqPosTE - 1) * $teWidth + ($eqWidthTE * $teWidth) / 2;
$lineStartY = $carrierY + $blockHeight;
$lineLength = 18;
$lineEndY = $lineStartY + $lineLength;
// Phase color
$phase = $conn->connection_type ?: 'L1N';
$color = $phaseColors[$phase] ?? $phaseColors['L1'] ?? array(139, 69, 19);
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
$pdf->SetFillColor($color[0], $color[1], $color[2]);
// Vertical line going down
$pdf->Line($eqCenterX, $lineStartY, $eqCenterX, $lineEndY);
// Arrow at end
$pdf->Polygon(array(
$eqCenterX - 1.5, $lineEndY - 2,
$eqCenterX, $lineEndY,
$eqCenterX + 1.5, $lineEndY - 2
), 'F');
// Left label: Bezeichnung (rotated text not easy in TCPDF, use horizontal)
if (!empty($conn->output_label)) {
$pdf->SetFont('dejavusans', 'B', 5);
$pdf->SetTextColor(0, 0, 0);
$pdf->SetXY($eqCenterX - 15, $lineEndY + 1);
$pdf->Cell(30, 3, dol_trunc($conn->output_label, 15), 0, 0, 'C');
}
// Right label: Kabeltyp + Größe
$cableInfo = trim(($conn->medium_type ?: '') . ' ' . ($conn->medium_spec ?: ''));
if (!empty($cableInfo)) {
$pdf->SetFont('dejavusans', '', 4);
$pdf->SetTextColor(100, 100, 100);
$pdf->SetXY($eqCenterX - 15, $lineEndY + 4);
$pdf->Cell(30, 3, dol_trunc($cableInfo, 18), 0, 0, 'C');
}
// Phase type
$pdf->SetFont('dejavusans', 'B', 5);
$pdf->SetTextColor($color[0], $color[1], $color[2]);
$pdf->SetXY($eqCenterX - 5, $lineEndY + 7);
$pdf->Cell(10, 3, $phase, 0, 0, 'C');
}
}
$carrierIndex++;
}
$pdf->SetTextColor(0, 0, 0);
// ========================================
// Legend - show phase colors used in busbars
// ========================================
$legendY = $startY + $height - 20;
$pdf->SetFont('dejavusans', '', 6);
$pdf->SetXY($startX + 5, $legendY);
$pdf->Cell(0, 4, 'Phasenfarben nach DIN VDE:', 0, 1, 'L');
$legendX = $startX + 5;
$phases = array('L1', 'L2', 'L3', 'N', 'PE');
foreach ($phases as $idx => $phase) {
$color = $phaseColors[$phase];
$pdf->SetFillColor($color[0], $color[1], $color[2]);
$pdf->Rect($legendX + $idx * 25, $legendY + 5, 8, 3, 'F');
$pdf->SetTextColor($color[0], $color[1], $color[2]);
$pdf->SetXY($legendX + $idx * 25 + 10, $legendY + 4);
$pdf->Cell(12, 4, $phase, 0, 0, 'L');
}
$pdf->SetTextColor(0, 0, 0);
}

View file

@ -40,6 +40,7 @@ class Equipment extends CommonObject
public $type_label_short;
public $type_color;
public $type_picto;
public $type_icon_file; // SVG/PNG schematic symbol
public $product_ref;
public $product_label;
public $protection_device_label; // Label of the protection device
@ -135,7 +136,8 @@ class Equipment extends CommonObject
public function fetch($id)
{
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.color as type_color, t.picto as type_picto,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " p.ref as product_ref, p.label as product_label,";
$sql .= " prot.label as protection_device_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
@ -168,8 +170,11 @@ class Equipment extends CommonObject
$this->type_label = $obj->type_label;
$this->type_label_short = $obj->type_label_short;
$this->type_ref = $obj->type_ref;
$this->type_color = $obj->type_color;
$this->type_picto = $obj->type_picto;
$this->type_icon_file = $obj->type_icon_file;
$this->terminals_config = $obj->terminals_config;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
$this->protection_device_label = $obj->protection_device_label;
@ -266,7 +271,7 @@ class Equipment extends CommonObject
$results = array();
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " prot.label as protection_device_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
@ -301,6 +306,7 @@ class Equipment extends CommonObject
$eq->type_ref = $obj->type_ref;
$eq->type_color = $obj->type_color;
$eq->type_picto = $obj->type_picto;
$eq->type_icon_file = $obj->type_icon_file;
$eq->terminals_config = $obj->terminals_config;
$eq->protection_device_label = $obj->protection_device_label;

View file

@ -44,6 +44,7 @@ class EquipmentConnection extends CommonObject
public $fk_carrier;
public $position_y = 0;
public $path_data; // SVG path for manually drawn connections
public $note_private;
public $status = 1;
@ -88,7 +89,7 @@ class EquipmentConnection extends CommonObject
$sql .= "entity, fk_source, source_terminal, source_terminal_id, fk_target, target_terminal, target_terminal_id,";
$sql .= " connection_type, color, output_label,";
$sql .= " medium_type, medium_spec, medium_length,";
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y,";
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y, path_data,";
$sql .= " note_private, status, date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $conf->entity);
@ -111,6 +112,7 @@ class EquipmentConnection extends CommonObject
$sql .= ", ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
$sql .= ", ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
$sql .= ", ".((int) $this->position_y);
$sql .= ", ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", ".((int) $this->status);
$sql .= ", '".$this->db->idate($now)."'";
@ -178,6 +180,7 @@ class EquipmentConnection extends CommonObject
$this->excluded_te = $obj->excluded_te;
$this->fk_carrier = $obj->fk_carrier;
$this->position_y = $obj->position_y;
$this->path_data = isset($obj->path_data) ? $obj->path_data : null;
$this->note_private = $obj->note_private;
$this->status = $obj->status;
$this->date_creation = $this->db->jdate($obj->date_creation);
@ -230,6 +233,7 @@ class EquipmentConnection extends CommonObject
$sql .= ", excluded_te = ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
$sql .= ", fk_carrier = ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
$sql .= ", position_y = ".((int) $this->position_y);
$sql .= ", path_data = ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", status = ".((int) $this->status);
$sql .= ", fk_user_modif = ".((int) $user->id);
@ -325,6 +329,7 @@ class EquipmentConnection extends CommonObject
$conn->excluded_te = $obj->excluded_te;
$conn->fk_carrier = $obj->fk_carrier;
$conn->position_y = $obj->position_y;
$conn->path_data = isset($obj->path_data) ? $obj->path_data : null;
$conn->status = $obj->status;
$conn->source_label = $obj->source_label;

View file

@ -29,6 +29,7 @@ class EquipmentType extends CommonObject
public $terminals_config; // JSON config for terminals
public $picto;
public $icon_file; // Uploaded SVG/PNG file for schematic symbol
public $is_system;
public $position;
public $active;
@ -76,7 +77,7 @@ class EquipmentType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " width_te, color, fk_product, terminals_config,";
$sql .= " picto, is_system, position, active,";
$sql .= " picto, icon_file, is_system, position, active,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= "0"; // entity 0 = global
@ -90,6 +91,7 @@ class EquipmentType extends CommonObject
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created
$sql .= ", ".((int) $this->position);
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
@ -148,6 +150,7 @@ class EquipmentType extends CommonObject
$this->fk_product = $obj->fk_product;
$this->terminals_config = $obj->terminals_config;
$this->picto = $obj->picto;
$this->icon_file = $obj->icon_file;
$this->is_system = $obj->is_system;
$this->position = $obj->position;
$this->active = $obj->active;
@ -195,6 +198,7 @@ class EquipmentType extends CommonObject
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", terminals_config = ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", icon_file = ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", position = ".((int) $this->position);
$sql .= ", active = ".((int) $this->active);
$sql .= ", fk_user_modif = ".((int) $user->id);
@ -303,6 +307,7 @@ class EquipmentType extends CommonObject
$type->color = $obj->color;
$type->fk_product = $obj->fk_product;
$type->picto = $obj->picto;
$type->icon_file = $obj->icon_file;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;

File diff suppressed because it is too large Load diff

1
js/pathfinding.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,12 @@
--
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
--
-- Add path_data column for manually drawn connection paths
ALTER TABLE llx_kundenkarte_equipment_connection ADD COLUMN IF NOT EXISTS path_data TEXT AFTER position_y;
-- Add icon_file column for custom SVG/PNG schematic symbols
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS icon_file VARCHAR(255) AFTER picto;
-- Add terminals_config if not exists
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS terminals_config TEXT AFTER fk_product;

View file

@ -37,6 +37,9 @@ CREATE TABLE llx_kundenkarte_equipment_connection (
fk_carrier INTEGER, -- Carrier where this connection is rendered
position_y INTEGER DEFAULT 0, -- Y offset for rendering (0=first row, 1=second row, etc.)
-- Manual path data (SVG path string for manually drawn connections)
path_data TEXT, -- SVG path like "M 100 200 L 150 200 L 150 300"
note_private TEXT,
status INTEGER DEFAULT 1,

View file

@ -24,7 +24,11 @@ CREATE TABLE llx_kundenkarte_equipment_type
-- Optionale Produkt-Verknuepfung
fk_product integer DEFAULT NULL COMMENT 'Optionales Standard-Dolibarr-Produkt',
-- Terminal-Konfiguration (JSON)
terminals_config text COMMENT 'JSON config for terminals',
picto varchar(64),
icon_file varchar(255) COMMENT 'Uploaded SVG/PNG file for schematic symbol',
is_system tinyint DEFAULT 0 NOT NULL,
position integer DEFAULT 0,

View file

@ -292,7 +292,7 @@ if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete)
// 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()));
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);
@ -548,91 +548,41 @@ if (empty($customerSystems)) {
// 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>';
print '<br><h4><i class="fa fa-microchip"></i> '.$langs->trans('Equipment').' - Schaltplan</h4>';
// Equipment container
// Equipment container - nur noch der SchematicEditor
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;">';
// Schematic Editor - Hauptansicht
print '<div class="schematic-editor-wrapper">';
print '<div class="schematic-editor-header" style="display:flex;justify-content:space-between;align-items:center;padding:10px 15px;background:#252525;border:1px solid #333;border-radius:4px 4px 0 0;">';
print '<div style="color:#3498db;">';
print '<strong>'.$langs->trans('ConnectionEditor').'</strong> <span style="color:#888;font-size:0.85em;">(Klick auf Terminal → Klick auf Ziel-Terminal = Verbindung | Rechtsklick = Löschen)</span>';
print '<strong>'.$langs->trans('SchematicEditor').'</strong> <span style="color:#888;font-size:0.85em;">(Klick auf Block = Bearbeiten | Drag = Verschieben | + = Hinzufügen)</span>';
print '</div>';
print '<div class="schematic-editor-actions" style="display:flex;gap:10px;">';
print '<div class="schematic-editor-actions" style="display:flex;gap:10px;align-items:center;">';
// Zoom controls
print '<div class="schematic-zoom-controls" style="display:flex;gap:2px;align-items:center;background:#222;border-radius:3px;padding:2px;">';
print '<button type="button" class="schematic-zoom-out" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#fff;cursor:pointer;" title="Verkleinern (Ctrl+Scroll)"><i class="fa fa-minus"></i></button>';
print '<span class="schematic-zoom-level" style="min-width:45px;text-align:center;color:#888;font-size:12px;">100%</span>';
print '<button type="button" class="schematic-zoom-in" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#fff;cursor:pointer;" title="Vergrößern (Ctrl+Scroll)"><i class="fa fa-plus"></i></button>';
print '<button type="button" class="schematic-zoom-fit" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;margin-left:3px;" title="Einpassen"><i class="fa fa-compress"></i></button>';
print '<button type="button" class="schematic-zoom-reset" style="width:28px;height:28px;background:#333;border:1px solid #555;border-radius:3px;color:#888;cursor:pointer;" title="100%"><i class="fa fa-search"></i></button>';
print '</div>';
// Manual wire draw toggle
print '<button type="button" class="schematic-wire-draw-toggle" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#27ae60;cursor:pointer;" title="Manueller Zeichenmodus: Leitungen selbst zeichnen mit Raster-Snap">';
print '<i class="fa fa-pencil"></i> Manuell zeichnen';
print '</button>';
print '<button type="button" class="schematic-add-busbar" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#f39c12;cursor:pointer;" title="Phasenschiene hinzufügen">';
print '<i class="fa fa-arrows-h"></i> Phasenschiene';
print '</button>';
print '<button type="button" class="schematic-clear-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#e74c3c;cursor:pointer;">';
print '<i class="fa fa-trash"></i> Alle Verbindungen löschen';
print '</button>';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '<a href="'.$pdfExportUrl.'" target="_blank" class="schematic-export-pdf" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="PDF Export (Leitungslaufplan nach DIN EN 61082)">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
print '</div>';
print '</div>';
print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">';
@ -642,402 +592,14 @@ if (empty($customerSystems)) {
print '</div>'; // .kundenkarte-equipment-container
// Initialize Equipment JavaScript
// Initialize SchematicEditor 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.');
}
if (typeof KundenKarte !== "undefined" && 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