diff --git a/admin/equipment_types.php b/admin/equipment_types.php index a343733..52fbd40 100644 --- a/admin/equipment_types.php +++ b/admin/equipment_types.php @@ -374,7 +374,7 @@ if (in_array($action, array('create', 'edit'))) { } print ''; - // Icon + // FontAwesome Icon (fallback) print ''.$langs->trans('SystemPicto').''; print '
'; print ''; @@ -384,7 +384,39 @@ if (in_array($action, array('create', 'edit'))) { print ''; print ''; print ''; - print '
'; + print ''; + print 'Fallback-Icon wenn kein Schaltzeichen hochgeladen'; + print ''; + + // Schaltzeichen Upload (SVG/PNG) + print ''.$langs->trans('SchematicSymbol').''; + print ''; + print '
'; + + // Preview area + print '
'; + if ($action == 'edit' && $equipmentType->icon_file) { + $iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($equipmentType->icon_file); + print 'Icon'; + } else { + print 'Kein
Symbol
'; + } + print '
'; + + // Upload controls + print '
'; + print ''; + print ''; + if ($action == 'edit' && $equipmentType->icon_file) { + print ' '; + } + print '
Normgerechte Symbole nach IEC 60617 / DIN EN 60617
'; + print '
'; + + print '
'; + print ''; // Position print ''.$langs->trans('Position').''; @@ -410,7 +442,8 @@ if (in_array($action, array('create', 'edit'))) { print ''; - // JavaScript for terminal presets + // JavaScript for terminal presets and icon upload + $typeIdJs = $action == 'edit' ? $typeId : 0; print ''; print '
'; diff --git a/ajax/equipment.php b/ajax/equipment.php index e93860b..d58ec97 100644 --- a/ajax/equipment.php +++ b/ajax/equipment.php @@ -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 { diff --git a/ajax/equipment_connection.php b/ajax/equipment_connection.php index 2d0173e..7617900 100644 --- a/ajax/equipment_connection.php +++ b/ajax/equipment_connection.php @@ -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); } } diff --git a/ajax/equipment_type_icon.php b/ajax/equipment_type_icon.php new file mode 100644 index 0000000..bb49998 --- /dev/null +++ b/ajax/equipment_type_icon.php @@ -0,0 +1,148 @@ +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('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(); diff --git a/ajax/export_schematic_pdf.php b/ajax/export_schematic_pdf.php new file mode 100644 index 0000000..077ed74 --- /dev/null +++ b/ajax/export_schematic_pdf.php @@ -0,0 +1,717 @@ +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('/]*>/', '', $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); +} diff --git a/class/equipment.class.php b/class/equipment.class.php index f03bf0f..55b9866 100644 --- a/class/equipment.class.php +++ b/class/equipment.class.php @@ -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; diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php index d2e7eea..c355b30 100644 --- a/class/equipmentconnection.class.php +++ b/class/equipmentconnection.class.php @@ -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; diff --git a/class/equipmenttype.class.php b/class/equipmenttype.class.php index 52e0257..2ca16d1 100644 --- a/class/equipmenttype.class.php +++ b/class/equipmenttype.class.php @@ -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; diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 1f8a41f..d8abef3 100755 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -9,6 +9,96 @@ // Namespace window.KundenKarte = window.KundenKarte || {}; + // =========================================== + // Global Dialog Functions (replacing browser dialogs) + // =========================================== + + // Escape HTML helper + function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Global alert dialog + KundenKarte.showAlert = function(title, message, onClose) { + $('#kundenkarte-alert-dialog').remove(); + + var html = '
'; + html += '
'; + html += '

' + escapeHtml(title) + '

'; + html += '×
'; + html += '
'; + html += '

' + escapeHtml(message) + '

'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-alert-dialog').addClass('visible'); + $('#alert-ok').focus(); + + var closeDialog = function() { + $('#kundenkarte-alert-dialog').remove(); + $(document).off('keydown.alertDialog'); + if (typeof onClose === 'function') onClose(); + }; + + $('#alert-ok, #kundenkarte-alert-dialog .kundenkarte-modal-close').on('click', closeDialog); + $(document).on('keydown.alertDialog', function(e) { + if (e.key === 'Escape' || e.key === 'Enter') closeDialog(); + }); + }; + + // Global confirm dialog + KundenKarte.showConfirm = function(title, message, onConfirm, onCancel) { + $('#kundenkarte-confirm-dialog').remove(); + + var html = '
'; + html += '
'; + html += '

' + escapeHtml(title) + '

'; + html += '×
'; + html += '
'; + html += '

' + escapeHtml(message) + '

'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-confirm-dialog').addClass('visible'); + $('#confirm-yes').focus(); + + $('#confirm-yes').on('click', function() { + $('#kundenkarte-confirm-dialog').remove(); + $(document).off('keydown.confirmDialog'); + if (typeof onConfirm === 'function') onConfirm(); + }); + + $('#confirm-no, #kundenkarte-confirm-dialog .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-confirm-dialog').remove(); + $(document).off('keydown.confirmDialog'); + if (typeof onCancel === 'function') onCancel(); + }); + + $(document).on('keydown.confirmDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-confirm-dialog').remove(); + $(document).off('keydown.confirmDialog'); + if (typeof onCancel === 'function') onCancel(); + } else if (e.key === 'Enter') { + $('#kundenkarte-confirm-dialog').remove(); + $(document).off('keydown.confirmDialog'); + if (typeof onConfirm === 'function') onConfirm(); + } + }); + }; + // Get base URL for AJAX calls var baseUrl = (typeof DOL_URL_ROOT !== 'undefined') ? DOL_URL_ROOT : ''; if (!baseUrl) { @@ -795,7 +885,7 @@ self.selectIcon(response.icon.url, 'custom'); }, 500); } else { - alert('Fehler: ' + (response.error || 'Unbekannter Fehler')); + KundenKarte.showAlert('Fehler', response.error || 'Unbekannter Fehler'); } }, error: function(xhr) { @@ -804,7 +894,7 @@ var resp = JSON.parse(xhr.responseText); msg = resp.error || msg; } catch(e) {} - alert(msg); + KundenKarte.showAlert('Fehler', msg); } }); @@ -824,7 +914,7 @@ if (response.success) { self.loadCustomIcons(); } else { - alert('Fehler: ' + (response.error || 'Löschen fehlgeschlagen')); + KundenKarte.showAlert('Fehler', response.error || 'Löschen fehlgeschlagen'); } } }); @@ -1481,13 +1571,13 @@ location.reload(); } else { $('#panel-save').prop('disabled', false); - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#panel-save').prop('disabled', false); - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -1532,11 +1622,11 @@ if (response.success) { location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -1555,11 +1645,11 @@ if (response.success) { location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -1712,13 +1802,13 @@ location.reload(); } else { $('#carrier-save').prop('disabled', false); - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#carrier-save').prop('disabled', false); - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -1984,7 +2074,7 @@ var protectionLabel = $('input[name="equipment_protection_label"]').val(); if (!typeId) { - alert('Bitte wählen Sie einen Typ.'); + KundenKarte.showAlert('Hinweis', 'Bitte wählen Sie einen Typ.'); return; } @@ -2027,13 +2117,13 @@ location.reload(); } else { $('#equipment-save').prop('disabled', false); - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#equipment-save').prop('disabled', false); - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -2058,12 +2148,12 @@ if (response.success) { location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -2108,7 +2198,7 @@ if (response.success) { location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } } }); @@ -2302,11 +2392,11 @@ $('#kundenkarte-output-dialog').remove(); location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -2387,7 +2477,7 @@ // Multi-phase rail options html += '
'; html += '
'; - html += ''; + html += ''; html += ''; html += ''; html += ''; @@ -2676,11 +2766,11 @@ $('#kundenkarte-rail-dialog').remove(); location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -2699,11 +2789,11 @@ if (response.success && response.connection) { self.renderEditOutputDialog(connectionId, carrierId, response.connection); } else { - alert('Fehler: Verbindung nicht gefunden'); + KundenKarte.showAlert('Fehler', 'Verbindung nicht gefunden'); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -2839,11 +2929,11 @@ $('#kundenkarte-output-dialog').remove(); location.reload(); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -3616,11 +3706,11 @@ if (response.success) { self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -3751,11 +3841,11 @@ $('#kundenkarte-busbar-dialog').remove(); self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } }, error: function() { - alert('Netzwerkfehler'); + KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, @@ -3878,7 +3968,7 @@ $('#kundenkarte-busbar-dialog').remove(); self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } } }); @@ -3979,7 +4069,7 @@ var targetId = $('select[name="conn_target"]').val(); if (!sourceId || !targetId) { - alert('Bitte Quelle und Ziel auswählen'); + KundenKarte.showAlert('Hinweis', 'Bitte Quelle und Ziel auswählen'); return; } @@ -4007,7 +4097,7 @@ $('#kundenkarte-conn-dialog').remove(); self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } } }); @@ -4110,7 +4200,7 @@ $('#kundenkarte-conn-dialog').remove(); self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + KundenKarte.showAlert('Fehler', response.error); } } }); @@ -4119,8 +4209,6 @@ deleteConnection: function(connectionId, carrierId) { var self = this; - if (!confirm('Verbindung wirklich löschen?')) return; - $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', method: 'POST', @@ -4134,7 +4222,7 @@ if (response.success) { self.loadAndRenderEditor(carrierId); } else { - alert('Fehler: ' + response.error); + self.showMessage(response.error || 'Fehler beim Löschen', 'error'); } } }); @@ -4307,17 +4395,20 @@ * - Orthogonale Verbindungspfade */ KundenKarte.SchematicEditor = { - // Constants - TE_WIDTH: 40, - RAIL_HEIGHT: 8, - RAIL_SPACING: 160, // Abstand zwischen Hutschienen - BLOCK_HEIGHT: 80, - TERMINAL_RADIUS: 6, - GRID_SIZE: 10, - TOP_MARGIN: 80, // Platz oben für Verbindungen - BOTTOM_MARGIN: 60, // Platz unten - PANEL_GAP: 40, // Abstand zwischen Panels - PANEL_PADDING: 20, // Innenabstand Panel + // Constants (40% larger for better visibility) + TE_WIDTH: 56, + RAIL_HEIGHT: 14, // Hutschiene Höhe + RAIL_SPACING: 308, // Abstand zwischen Hutschienen (Platz für Blöcke + Verbindungen) + BLOCK_HEIGHT: 112, + TERMINAL_RADIUS: 8, + GRID_SIZE: 14, + MIN_TOP_MARGIN: 100, // Minimaler Platz oben für Panel (inkl. Hauptsammelschienen) + MAIN_BUSBAR_HEIGHT: 60, // Höhe des Hauptsammelschienen-Bereichs (L1,L2,L3,N,PE) + BLOCK_INNER_OFFSET: 80, // Zusätzlicher Offset für Blöcke innerhalb des Panels + BOTTOM_MARGIN: 56, // Platz unten + PANEL_GAP: 56, // Abstand zwischen Panels + PANEL_PADDING: 28, // Innenabstand Panel + CONNECTION_ROUTE_SPACE: 17, // Platz pro Verbindungsroute // Colors COLORS: { @@ -4351,14 +4442,22 @@ selectedTerminal: null, dragState: null, isInitialized: false, + isLoading: false, svgElement: null, scale: 1, + // Manual wire drawing mode + wireDrawMode: false, + wireDrawPoints: [], // Array of {x, y} points for current wire + wireDrawSourceEq: null, // Source equipment ID + wireDrawSourceTerm: null, // Source terminal ID + WIRE_GRID_SIZE: 25, // Snap grid size in pixels (larger = fewer points) + // Default terminal configs for common types // Terminals are bidirectional - no strict input/output DEFAULT_TERMINALS: { 'LS': { terminals: [{id: 't1', label: '●', pos: 'top'}, {id: 't2', label: '●', pos: 'bottom'}] }, - 'FI': { terminals: [{id: 't1', label: 'L', pos: 'top'}, {id: 't2', label: 'N', pos: 'top'}, {id: 't3', label: 'L', pos: 'bottom'}, {id: 't4', label: 'N', pos: 'bottom'}] }, + 'FI': { terminals: [{id: 't1', label: 'L1', pos: 'top'}, {id: 't2', label: 'L2', pos: 'top'}, {id: 't3', label: 'L3', pos: 'top'}, {id: 't4', label: 'N', pos: 'top'}, {id: 't5', label: 'L1', pos: 'bottom'}, {id: 't6', label: 'L2', pos: 'bottom'}, {id: 't7', label: 'L3', pos: 'bottom'}, {id: 't8', label: 'N', pos: 'bottom'}] }, 'FI4P': { terminals: [{id: 't1', label: 'L1', pos: 'top'}, {id: 't2', label: 'L2', pos: 'top'}, {id: 't3', label: 'L3', pos: 'top'}, {id: 't4', label: 'N', pos: 'top'}, {id: 't5', label: 'L1', pos: 'bottom'}, {id: 't6', label: 'L2', pos: 'bottom'}, {id: 't7', label: 'L3', pos: 'bottom'}, {id: 't8', label: 'N', pos: 'bottom'}] }, 'SCHUETZ': { terminals: [{id: 't1', label: 'A1', pos: 'top'}, {id: 't2', label: '1', pos: 'top'}, {id: 't3', label: 'A2', pos: 'bottom'}, {id: 't4', label: '2', pos: 'bottom'}] }, 'KLEMME': { terminals: [{id: 't1', label: '●', pos: 'top'}, {id: 't2', label: '●', pos: 'bottom'}] }, @@ -4375,53 +4474,368 @@ bindEvents: function() { var self = this; - // Terminal click - start/end connection + // Terminal click - only used in wire draw mode $(document).off('click.terminal').on('click.terminal', '.schematic-terminal', function(e) { e.preventDefault(); e.stopPropagation(); - self.handleTerminalClick($(this)); + // Only handle in manual wire draw mode + if (self.wireDrawMode) { + self.handleTerminalClick($(this)); + } }); - // Block drag + // Block drag - track if dragged to distinguish from click $(document).off('mousedown.blockDrag').on('mousedown.blockDrag', '.schematic-block', function(e) { if ($(e.target).hasClass('schematic-terminal')) return; e.preventDefault(); + self.blockDragStartPos = { x: e.clientX, y: e.clientY }; + self.blockWasDragged = false; + self.clickedEquipmentId = $(this).data('equipment-id'); self.startDragBlock($(this), e); }); $(document).off('mousemove.blockDrag').on('mousemove.blockDrag', function(e) { if (self.dragState && self.dragState.type === 'block') { + // Check if moved more than 5px - then it's a drag, not a click + if (self.blockDragStartPos) { + var dx = Math.abs(e.clientX - self.blockDragStartPos.x); + var dy = Math.abs(e.clientY - self.blockDragStartPos.y); + if (dx > 5 || dy > 5) { + self.blockWasDragged = true; + } + } self.updateDragBlock(e); } }); $(document).off('mouseup.blockDrag').on('mouseup.blockDrag', function(e) { + var equipmentId = self.clickedEquipmentId; + if (self.dragState && self.dragState.type === 'block') { self.endDragBlock(e); } + + // If not dragged, show popup + if (!self.blockWasDragged && equipmentId) { + self.showEquipmentPopup(equipmentId, e.clientX, e.clientY); + } + + self.blockDragStartPos = null; + self.blockWasDragged = false; + self.clickedEquipmentId = null; }); - // Connection right-click to delete - $(document).off('contextmenu.connection').on('contextmenu.connection', '.schematic-connection', function(e) { - e.preventDefault(); - var connId = $(this).data('connection-id'); - if (confirm('Verbindung löschen?')) { - self.deleteConnection(connId); + // Hide popups when clicking elsewhere + $(document).off('mousedown.hidePopup').on('mousedown.hidePopup', function(e) { + // Don't hide if clicking on popup buttons + if ($(e.target).closest('.schematic-connection-popup, .schematic-equipment-popup').length) { + return; } + // Don't hide if clicking on SVG elements (handlers will handle it) + if ($(e.target).closest('svg').length) { + return; + } + self.hideConnectionPopup(); + self.hideEquipmentPopup(); }); // Clear all connections $(document).off('click.clearConns').on('click.clearConns', '.schematic-clear-connections', function(e) { e.preventDefault(); - if (confirm('Alle Verbindungen löschen?')) { + self.showConfirmDialog('Alle löschen', 'Alle Verbindungen wirklich löschen?', function() { self.clearAllConnections(); + }); + }); + + // Escape key - no longer needed for auto-selection, wire draw has its own handler + + // Zoom with mouse wheel + $(document).off('wheel.schematicZoom').on('wheel.schematicZoom', '.schematic-editor-canvas', function(e) { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + var delta = e.originalEvent.deltaY > 0 ? -0.1 : 0.1; + self.setZoom(self.scale + delta); } }); - // Escape to cancel selection - $(document).off('keydown.schematic').on('keydown.schematic', function(e) { - if (e.key === 'Escape' && self.selectedTerminal) { - self.cancelSelection(); + // Zoom buttons + $(document).off('click.zoomIn').on('click.zoomIn', '.schematic-zoom-in', function(e) { + e.preventDefault(); + self.setZoom(self.scale + 0.1); + }); + $(document).off('click.zoomOut').on('click.zoomOut', '.schematic-zoom-out', function(e) { + e.preventDefault(); + self.setZoom(self.scale - 0.1); + }); + $(document).off('click.zoomReset').on('click.zoomReset', '.schematic-zoom-reset', function(e) { + e.preventDefault(); + self.setZoom(1); + }); + $(document).off('click.zoomFit').on('click.zoomFit', '.schematic-zoom-fit', function(e) { + e.preventDefault(); + self.zoomToFit(); + }); + + // Busbar click - show edit/delete popup + $(document).off('click.busbar').on('click.busbar', '.schematic-busbar', function(e) { + e.preventDefault(); + e.stopPropagation(); + var connectionId = $(this).data('connection-id'); + self.showBusbarPopup(connectionId, e.clientX, e.clientY); + }); + + // Terminal right-click - show output/cable dialog + $(document).off('contextmenu.terminal').on('contextmenu.terminal', '.schematic-terminal', function(e) { + e.preventDefault(); + e.stopPropagation(); + var $terminal = $(this); + var eqId = $terminal.data('equipment-id'); + var termId = $terminal.data('terminal-id'); + self.showOutputDialog(eqId, termId, e.clientX, e.clientY); + }); + + // Toggle manual wire draw mode + $(document).off('click.toggleWireDraw').on('click.toggleWireDraw', '.schematic-wire-draw-toggle', function(e) { + e.preventDefault(); + self.toggleWireDrawMode(); + }); + + // SVG click for wire drawing - add waypoint + $(document).off('click.wireDrawSvg').on('click.wireDrawSvg', '.schematic-editor-canvas svg', function(e) { + if (!self.wireDrawMode) return; + // Only add waypoints after source terminal is selected + if (!self.wireDrawSourceEq) { + // Show hint if clicking on canvas without selecting terminal first + if (!$(e.target).closest('.schematic-terminal').length) { + self.showMessage('Zuerst ein START-Terminal anklicken!', 'warning'); + } + return; + } + // Don't add point if clicking on terminal or block + if ($(e.target).closest('.schematic-terminal, .schematic-block').length) return; + + var svg = self.svgElement; + var pt = svg.createSVGPoint(); + pt.x = e.clientX; + pt.y = e.clientY; + var svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); + + // Snap to nearest terminal-aligned grid point + var snapped = self.snapToTerminalGrid(svgP.x, svgP.y); + + self.wireDrawPoints.push({x: snapped.x, y: snapped.y}); + self.updateWirePreview(); + console.log('Wire point added:', snapped.x, snapped.y, 'Total points:', self.wireDrawPoints.length); + }); + + // SVG mousemove for wire preview - show cursor and preview line + $(document).off('mousemove.wireDraw').on('mousemove.wireDraw', '.schematic-editor-canvas svg', function(e) { + if (!self.wireDrawMode) return; + + var svg = self.svgElement; + var pt = svg.createSVGPoint(); + pt.x = e.clientX; + pt.y = e.clientY; + var svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); + + // Snap to nearest terminal-aligned grid point + var snapped = self.snapToTerminalGrid(svgP.x, svgP.y); + + // Always update cursor position (even before source is selected) + self.updateWirePreviewCursor(snapped.x, snapped.y); + }); + + // Right-click to cancel wire drawing completely + $(document).off('contextmenu.wireDraw').on('contextmenu.wireDraw', '.schematic-editor-canvas svg', function(e) { + if (!self.wireDrawMode) return; + e.preventDefault(); + if (self.wireDrawSourceEq) { + // Cancel entire drawing + self.cancelWireDrawing(); + } + }); + + // Escape to cancel wire drawing + $(document).off('keydown.wireDraw').on('keydown.wireDraw', function(e) { + if (e.key === 'Escape' && self.wireDrawMode && self.wireDrawSourceEq) { + self.cancelWireDrawing(); + } + }); + }, + + showBusbarPopup: function(connectionId, x, y) { + var self = this; + this.hideBusbarPopup(); + + var conn = this.connections.find(function(c) { return String(c.id) === String(connectionId); }); + if (!conn) return; + + var html = '
'; + html += '
Sammelschiene
'; + html += '
' + (conn.rail_phases || conn.connection_type || 'Unbekannt') + '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.busbar-edit-btn').on('click', function() { + var id = $(this).data('id'); + self.hideBusbarPopup(); + self.showEditBusbarDialog(id); + }); + + $('.busbar-delete-btn').on('click', function() { + var id = $(this).data('id'); + self.hideBusbarPopup(); + self.deleteBusbar(id); + }); + + // Close on click outside + setTimeout(function() { + $(document).one('click', function() { + self.hideBusbarPopup(); + }); + }, 100); + }, + + hideBusbarPopup: function() { + $('.schematic-busbar-popup').remove(); + }, + + showEditBusbarDialog: function(connectionId) { + var self = this; + var conn = this.connections.find(function(c) { return String(c.id) === String(connectionId); }); + if (!conn) return; + + var carrier = this.carriers.find(function(c) { return String(c.id) === String(conn.fk_carrier); }); + var totalTE = carrier ? (parseInt(carrier.total_te) || 12) : 12; + + var html = '
'; + html += '
'; + html += '

Sammelschiene bearbeiten

'; + + // Phase type selection + html += '
'; + html += '
'; + + // Start TE + html += '
'; + html += '
'; + + // End TE + html += '
'; + html += '
'; + + // Position (above/below) + html += '
'; + html += '
'; + + // Excluded TEs + html += '
'; + html += '
'; + + // Color + html += '
'; + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.dialog-cancel, .schematic-dialog-overlay').on('click', function() { + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + + $('.dialog-save').on('click', function() { + var newPhases = $('.dialog-busbar-phases').val(); + var startTE = parseInt($('.dialog-busbar-start').val()) || 1; + var endTE = parseInt($('.dialog-busbar-end').val()) || totalTE; + var newPosY = parseInt($('.dialog-busbar-position').val()) || 0; + var excludedTE = $('.dialog-busbar-excluded').val() || ''; + var color = $('.dialog-busbar-color').val(); + + self.updateBusbar(connectionId, newPhases, startTE, endTE, newPosY, color, excludedTE); + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + }, + + updateBusbar: function(connectionId, phases, startTE, endTE, positionY, color, excludedTE) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: connectionId, + rail_start_te: startTE, + rail_end_te: endTE, + rail_phases: phases, + position_y: positionY, + color: color, + excluded_te: excludedTE, + connection_type: phases, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Sammelschiene aktualisiert', 'success'); + // Update local data + var conn = self.connections.find(function(c) { return String(c.id) === String(connectionId); }); + if (conn) { + conn.rail_start_te = startTE; + conn.rail_end_te = endTE; + conn.rail_phases = phases; + conn.position_y = positionY; + conn.color = color; + conn.excluded_te = excludedTE; + conn.connection_type = phases; + } + self.render(); + } else { + self.showMessage(response.error || 'Fehler beim Aktualisieren', 'error'); + } + } + }); + }, + + deleteBusbar: function(connectionId) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'delete', + connection_id: connectionId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Sammelschiene gelöscht', 'success'); + // Remove from local data + self.connections = self.connections.filter(function(c) { return String(c.id) !== String(connectionId); }); + self.render(); + } else { + self.showMessage(response.error || 'Fehler beim Löschen', 'error'); + } } }); }, @@ -4429,6 +4843,14 @@ loadData: function() { var self = this; + // Prevent multiple simultaneous loads (race condition fix) + if (this.isLoading) { + console.log('SchematicEditor loadData: Already loading, skipping...'); + return; + } + this.isLoading = true; + console.log('SchematicEditor loadData: Starting...'); + // Load panels with carriers for this anlage $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', @@ -4437,6 +4859,7 @@ success: function(response) { if (response.success) { self.panels = response.panels || []; + console.log('SchematicEditor loadData: Loaded ' + self.panels.length + ' panels'); // Flatten carriers from all panels self.carriers = []; self.panels.forEach(function(panel) { @@ -4447,8 +4870,16 @@ }); } }); + console.log('SchematicEditor loadData: Loaded ' + self.carriers.length + ' carriers'); self.loadEquipment(); + } else { + console.error('SchematicEditor loadData: Failed to load panels', response); + self.isLoading = false; } + }, + error: function(xhr, status, error) { + console.error('SchematicEditor loadData: AJAX error', error); + self.isLoading = false; } }); }, @@ -4478,6 +4909,9 @@ $.when.apply($, promises).then(function() { self.loadConnections(); + }).fail(function() { + console.error('SchematicEditor loadEquipment: One or more AJAX requests failed'); + self.isLoading = false; }); }, @@ -4491,6 +4925,20 @@ success: function(response) { if (response.success) { self.connections = response.connections || []; + + // Restore any manually drawn paths + if (self._manualPaths) { + self.connections.forEach(function(conn) { + if (self._manualPaths[conn.id]) { + conn._manualPath = self._manualPaths[conn.id]; + } + }); + } + + console.log('SchematicEditor: Loaded ' + self.connections.length + ' connections', self.connections); + console.log('SchematicEditor: Equipment count: ' + self.equipment.length, self.equipment); + } else { + console.error('SchematicEditor: Failed to load connections', response); } // Initialize canvas now that all data is loaded if (!self.isInitialized) { @@ -4498,30 +4946,48 @@ } else { self.render(); } + // Reset loading flag + self.isLoading = false; + }, + error: function(xhr, status, error) { + console.error('SchematicEditor: AJAX error loading connections', error); + self.isLoading = false; } }); }, - initCanvas: function() { + calculateLayout: function() { var self = this; - var $canvas = $('.schematic-editor-canvas'); - if (!$canvas.length) return; + // Calculate dynamic TOP_MARGIN based on number of connections going up + // Count connections that route above blocks (top-to-top connections) + var topConnections = 0; + this.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) !== 1 && conn.fk_source && conn.fk_target) { + topConnections++; + } + }); + // Panel top margin (where the panel border starts) + this.panelTopMargin = this.MIN_TOP_MARGIN; + // Block top margin = panel margin + inner offset + connection routing space + this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + topConnections * this.CONNECTION_ROUTE_SPACE; // Calculate canvas size based on panels // Panels nebeneinander, Hutschienen pro Panel untereinander var totalWidth = this.PANEL_PADDING; var maxPanelHeight = 0; - this.panels.forEach(function(panel, panelIdx) { + this.panels.forEach(function(panel) { var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; }); var maxTE = 12; panelCarriers.forEach(function(c) { if ((c.total_te || 12) > maxTE) maxTE = c.total_te; }); - var panelWidth = maxTE * self.TE_WIDTH + self.PANEL_PADDING * 2; - var panelHeight = self.TOP_MARGIN + panelCarriers.length * self.RAIL_SPACING + self.BOTTOM_MARGIN; + // Panel width includes: left margin for label (60px) + rail overhang (30px) + TE width + rail overhang (30px) + padding + var panelWidth = 60 + 30 + maxTE * self.TE_WIDTH + 30 + self.PANEL_PADDING; + // Use calculatedTopMargin for dynamic spacing + var panelHeight = self.calculatedTopMargin + self.BLOCK_HEIGHT + 20 + panelCarriers.length * self.RAIL_SPACING + self.BOTTOM_MARGIN; panel._x = totalWidth; panel._width = panelWidth; @@ -4541,14 +5007,23 @@ if ((c.total_te || 12) > fallbackMaxTE) fallbackMaxTE = c.total_te; }); totalWidth = fallbackMaxTE * self.TE_WIDTH + 150; - totalHeight = self.TOP_MARGIN + this.carriers.length * self.RAIL_SPACING + self.BOTTOM_MARGIN; + totalHeight = self.calculatedTopMargin + self.BLOCK_HEIGHT + 20 + this.carriers.length * self.RAIL_SPACING + self.BOTTOM_MARGIN; } - totalWidth = Math.max(totalWidth, 800); - totalHeight = Math.max(totalHeight, 400); + this.layoutWidth = Math.max(totalWidth, 800); + this.layoutHeight = Math.max(totalHeight, 400); + }, - // Create SVG - var svg = ''; + initCanvas: function() { + var $canvas = $('.schematic-editor-canvas'); + + if (!$canvas.length) return; + + // Calculate layout first + this.calculateLayout(); + + // Create SVG using calculated dimensions + var svg = ''; // Defs for markers and patterns svg += ''; @@ -4561,10 +5036,11 @@ // Background grid svg += ''; - // Layers + // Layers (order matters: back to front) svg += ''; - svg += ''; svg += ''; + svg += ''; // Distribution busbars (Phasenschienen) + svg += ''; svg += ''; // Connection preview line @@ -4572,27 +5048,32 @@ svg += ''; - $canvas.html(svg); + // Wrap SVG in zoom wrapper for coordinated scaling with controls + $canvas.html('
' + svg + '
'); this.svgElement = $canvas.find('.schematic-svg')[0]; this.isInitialized = true; // Render content this.render(); - // Track mouse for connection preview - $canvas.on('mousemove', function(e) { - if (self.selectedTerminal) { - self.updateConnectionPreview(e); - } - }); }, render: function() { if (!this.isInitialized) return; + // Recalculate layout (panels may have changed) + this.calculateLayout(); + + // Update SVG size + var $svg = $(this.svgElement); + $svg.attr('width', this.layoutWidth); + $svg.attr('height', this.layoutHeight); + this.renderRails(); this.renderBlocks(); + this.renderBusbars(); this.renderConnections(); + this.renderControls(); }, renderRails: function() { @@ -4608,72 +5089,87 @@ var panelX = panel._x || self.PANEL_PADDING; var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; }); - // Panel background + // Panel background - starts at panel top margin (before block offset) if (panel._width && panel._height) { - html += ''; + var panelTop = self.panelTopMargin; + var panelContentHeight = panel._height - self.panelTopMargin; + + html += ''; // Panel label - html += ''; - // Rail - html += ''; - // Rail label (links) - html += ''; + // Rail label (links vom Überstand, centered with rail) + html += ''; html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1)); html += ''; - // TE markers + // TE markers on the rail for (var te = 0; te <= (carrier.total_te || 12); te++) { var teX = x + te * self.TE_WIDTH; - html += ''; + html += ''; } }); }); } else { // Fallback: Carriers ohne Panels this.carriers.forEach(function(carrier, idx) { - var y = self.TOP_MARGIN + idx * self.RAIL_SPACING; + var blockTop = self.calculatedTopMargin + idx * self.RAIL_SPACING; + var railY = blockTop + self.BLOCK_HEIGHT + 10; var width = (carrier.total_te || 12) * self.TE_WIDTH; var x = self.PANEL_PADDING + 50; - carrier._y = y; + carrier._y = railY; carrier._x = x; + carrier._blockTop = blockTop; - html += ''; - html += ''; - html += ''; + html += ''; html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1)); html += ''; for (var te = 0; te <= (carrier.total_te || 12); te++) { var teX = x + te * self.TE_WIDTH; - html += ''; + html += ''; } }); } @@ -4691,14 +5187,26 @@ var blockHtml = ''; var terminalHtml = ''; + console.log('SchematicEditor renderBlocks: Processing ' + this.equipment.length + ' equipment'); + this.equipment.forEach(function(eq) { - var carrier = self.carriers.find(function(c) { return c.id == eq.carrier_id; }); - if (!carrier) return; + var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); }); + if (!carrier) { + console.log(' Equipment #' + eq.id + ' (' + eq.label + '): No carrier found for carrier_id=' + eq.carrier_id); + return; + } + if (typeof carrier._x === 'undefined' || carrier._x === null) { + console.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping'); + return; + } var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4; var blockHeight = self.BLOCK_HEIGHT; - var x = carrier._x + (eq.position_te - 1) * self.TE_WIDTH + 2; - var y = carrier._y - blockHeight / 2 - 20; + var x = parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * self.TE_WIDTH + 2; + // Position blocks centered on the rail (Hutschiene) + // Rail is at carrier._y, block center should align with rail center + var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; + var y = railCenterY - blockHeight / 2; // Store position eq._x = x; @@ -4713,17 +5221,17 @@ // Block background with gradient blockHtml += ''; + blockHtml += 'fill="' + color + '" stroke="#222" stroke-width="1.5" rx="4"/>'; // Label var labelY = blockHeight / 2; - blockHtml += ''; + blockHtml += ''; blockHtml += self.escapeHtml(eq.type_label_short || eq.label || ''); blockHtml += ''; // Additional info line if (eq.label && eq.type_label_short) { - blockHtml += ''; + blockHtml += ''; blockHtml += self.escapeHtml(eq.label); blockHtml += ''; } @@ -4731,40 +5239,53 @@ blockHtml += ''; // Terminals (bidirectional) + // WICHTIG: Terminals werden im festen TE-Raster platziert + // Jeder Terminal nimmt 1 TE ein - ein 3TE Block mit 3 Terminals + // sieht genauso aus wie 3 einzelne 1TE Blöcke var terminals = self.getTerminals(eq); var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; }); var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; }); - var topSpacing = blockWidth / (topTerminals.length + 1); - var bottomSpacing = blockWidth / (bottomTerminals.length + 1); + var widthTE = parseInt(eq.width_te) || 1; - // Top terminals - topTerminals.forEach(function(term, idx) { - var tx = x + topSpacing * (idx + 1); - var ty = y - 5; - var termColor = self.PHASE_COLORS[term.label] || '#3498db'; + // Check if this equipment is covered by a busbar (hide top terminals if so) + var coveredByBusbar = self.isEquipmentCoveredByBusbar(eq); - terminalHtml += ''; + // Top terminals - im TE-Raster platziert (hide if busbar covers this equipment) + if (!coveredByBusbar) { + topTerminals.forEach(function(term, idx) { + // Berechne welches TE dieser Terminal belegt (0-basiert) + var teIndex = idx % widthTE; + // Terminal ist in der Mitte des jeweiligen TE + var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); + var ty = y - 7; + var termColor = self.PHASE_COLORS[term.label] || '#3498db'; - terminalHtml += ''; - terminalHtml += '' + self.escapeHtml(term.label) + ''; - terminalHtml += ''; - }); + terminalHtml += ''; - // Bottom terminals + terminalHtml += ''; + terminalHtml += '' + self.escapeHtml(term.label) + ''; + terminalHtml += ''; + }); + } + + // Bottom terminals - im TE-Raster platziert bottomTerminals.forEach(function(term, idx) { - var tx = x + bottomSpacing * (idx + 1); - var ty = y + blockHeight + 5; + // Berechne welches TE dieser Terminal belegt (0-basiert) + var teIndex = idx % widthTE; + // Terminal ist in der Mitte des jeweiligen TE + var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); + var ty = y + blockHeight + 7; var termColor = self.PHASE_COLORS[term.label] || '#27ae60'; terminalHtml += ''; - terminalHtml += ''; - terminalHtml += '' + self.escapeHtml(term.label) + ''; + terminalHtml += ''; + terminalHtml += '' + self.escapeHtml(term.label) + ''; terminalHtml += ''; }); }); @@ -4773,74 +5294,1079 @@ $terminalLayer.html(terminalHtml); }, + renderBusbars: function() { + // Render Sammelschienen (busbars) - connections with is_rail=1 + var self = this; + var $layer = $(this.svgElement).find('.schematic-busbars-layer'); + $layer.empty(); + + var html = ''; + var renderedCount = 0; + + // Debug: list all connections with is_rail + var busbars = this.connections.filter(function(c) { return parseInt(c.is_rail) === 1; }); + console.log('SchematicEditor renderBusbars: Found ' + busbars.length + ' busbars in connections:', busbars); + console.log('SchematicEditor renderBusbars: Carriers available:', this.carriers.map(function(c) { return {id: c.id, _x: c._x, _y: c._y}; })); + + // First, group busbars by carrier and position for stacking + var busbarsByCarrierAndPos = {}; + this.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) !== 1) return; + var key = conn.fk_carrier + '_' + (parseInt(conn.position_y) || 0); + if (!busbarsByCarrierAndPos[key]) busbarsByCarrierAndPos[key] = []; + busbarsByCarrierAndPos[key].push(conn); + }); + + this.connections.forEach(function(conn) { + // Only process rails/busbars + if (parseInt(conn.is_rail) !== 1) { + return; + } + + // Get carrier for this busbar + var carrier = self.carriers.find(function(c) { + return String(c.id) === String(conn.fk_carrier); + }); + + if (!carrier || typeof carrier._x === 'undefined') { + console.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position'); + return; + } + + // Busbar spans from rail_start_te to rail_end_te + var startTE = parseInt(conn.rail_start_te) || 1; + var endTE = parseInt(conn.rail_end_te) || startTE + 1; + + // Calculate pixel positions + var startX = carrier._x + (startTE - 1) * self.TE_WIDTH; + var endX = carrier._x + endTE * self.TE_WIDTH; + var width = endX - startX; + + // Position busbar above or below the blocks based on position_y + // position_y: 0 = above (top), 1 = below (bottom) + var posY = parseInt(conn.position_y) || 0; + var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; + var blockTop = railCenterY - self.BLOCK_HEIGHT / 2; + var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2; + + // Count how many busbars are already at this position (to stack them) + var key = conn.fk_carrier + '_' + posY; + var carrierBusbars = busbarsByCarrierAndPos[key] || []; + var busbarIndex = carrierBusbars.indexOf(conn); + if (busbarIndex < 0) busbarIndex = 0; + + // Busbar height and position - stack multiple busbars + var busbarHeight = 24; + var busbarSpacing = busbarHeight + 20; // Space between stacked busbars (including label space) + var busbarY; + if (posY === 0) { + // Above blocks - stack upwards + busbarY = blockTop - busbarHeight - 25 - (busbarIndex * busbarSpacing); + } else { + // Below blocks - stack downwards + busbarY = blockBottom + 25 + (busbarIndex * busbarSpacing); + } + + // Color from connection or default phase color + var color = conn.color || self.PHASE_COLORS[conn.connection_type] || '#e74c3c'; + + // Draw busbar as rounded rectangle + html += ''; + + // Shadow + html += ''; + + // Parse excluded TEs + var excludedTEs = []; + if (conn.excluded_te) { + excludedTEs = conn.excluded_te.split(',').map(function(t) { return parseInt(t.trim()); }).filter(function(t) { return !isNaN(t); }); + } + + // Determine phase labels based on rail_phases + var phases = conn.rail_phases || conn.connection_type || ''; + var phaseLabels = []; + if (phases === 'L1L2L3') { + phaseLabels = ['L1', 'L2', 'L3']; + } else if (phases === 'L1') { + phaseLabels = ['L1']; + } else if (phases === 'L2') { + phaseLabels = ['L2']; + } else if (phases === 'L3') { + phaseLabels = ['L3']; + } else if (phases === 'N') { + phaseLabels = ['N']; + } else if (phases === 'PE') { + phaseLabels = ['PE']; + } else if (phases) { + phaseLabels = [phases]; + } + + // Draw taps per TE (not per block) - each TE gets its own connection point + // Phase index counts ALL TEs from start (including excluded ones) for correct phase assignment + for (var te = startTE; te <= endTE; te++) { + // Calculate phase for this TE position (based on position from start, not rendered count) + var teOffset = te - startTE; // 0-based offset from start + var currentPhase = phaseLabels.length > 0 ? phaseLabels[teOffset % phaseLabels.length] : ''; + + // Skip excluded TEs (but phase still counts) + if (excludedTEs.indexOf(te) !== -1) continue; + + // Calculate X position for this TE - center of the TE slot + // TE 1 starts at carrier._x, so center of TE 1 is at carrier._x + TE_WIDTH/2 + // TE n center is at carrier._x + (n-1) * TE_WIDTH + TE_WIDTH/2 + var teX = carrier._x + (te - 1) * self.TE_WIDTH + self.TE_WIDTH / 2; + + // Draw tap line from busbar to block/terminal area + var tapStartY = posY === 0 ? busbarY + busbarHeight : busbarY; + var tapEndY; + if (posY === 0) { + tapEndY = blockTop - 7; + } else { + tapEndY = blockBottom + 7; + } + + html += ''; + + // Connector dot at equipment end - same size as block terminals + html += ''; + + // Phase label at this TE connection + if (currentPhase) { + // Position label on the busbar + var phaseLabelY = busbarY + busbarHeight / 2 + 4; + html += ''; + + // Invisible hit area for clicking + var hitY = goingUp ? endY : startY; + html += ''; + + // Connection line + html += ''; + + // Arrow at end (pointing away from equipment) + if (goingUp) { + // Arrow pointing UP + html += ''; + } else { + // Arrow pointing DOWN + html += ''; + } + + // Labels - vertical text on both sides + var labelY = (startY + endY) / 2; + + // Left side: Bezeichnung (output_label) + if (conn.output_label) { + html += ''; + html += self.escapeHtml(conn.output_label); + html += ''; + } + + // Right side: Kabeltyp + Größe + var cableInfo = ''; + if (conn.medium_type) cableInfo = conn.medium_type; + if (conn.medium_spec) cableInfo += ' ' + conn.medium_spec; + if (cableInfo) { + html += ''; + html += self.escapeHtml(cableInfo.trim()); + html += ''; + } + + // Phase type at end of line + if (conn.connection_type) { + var phaseY = goingUp ? (endY - 10) : (endY + 14); + html += ''; + html += conn.connection_type; + html += ''; + } + + html += ''; + renderedCount++; + return; + } + + // ======================================== + // ANSCHLUSSPUNKT (Input) - no source, target exists + // Draw a line coming FROM ABOVE into the terminal + // All Anschlusspunkte use a uniform color (light blue) + // ======================================== + if (!conn.fk_source && targetEq) { + var targetTerminals = self.getTerminals(targetEq); + var targetTermId = conn.target_terminal_id || 't1'; + var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals); + if (!targetPos) return; + + // Uniform color for all Anschlusspunkte (inputs) + var inputColor = '#4fc3f7'; // Light blue - uniform for all inputs + + // Calculate line length based on label + var inputLabel = conn.output_label || ''; + var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30)); + var startY = targetPos.y - inputLineLength; + + // Draw vertical line coming down into terminal + var path = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y; + + html += ''; + + // Invisible hit area for clicking + html += ''; + + // Connection line + html += ''; + + // Circle at top (external source indicator) + html += ''; + + // Arrow pointing down into terminal + html += ''; + + // Phase label at top (big, prominent) + html += ''; + html += conn.connection_type || 'L1'; + html += ''; + + // Optional label on side (vertical) + if (conn.output_label) { + var labelY = targetPos.y - inputLineLength / 2; + html += ''; + html += self.escapeHtml(conn.output_label); + html += ''; + } + + html += ''; + renderedCount++; + return; + } + + // ======================================== + // NORMAL CONNECTION - both source and target exist + // ======================================== + if (!sourceEq || !targetEq) { + return; + } var sourceTerminals = self.getTerminals(sourceEq); var targetTerminals = self.getTerminals(targetEq); - // Find terminal positions - use first matching or default - var sourceTermId = conn.source_terminal_id || 't2'; // default bottom - var targetTermId = conn.target_terminal_id || 't1'; // default top + var sourceTermId = conn.source_terminal_id || 't2'; + var targetTermId = conn.target_terminal_id || 't1'; var sourcePos = self.getTerminalPosition(sourceEq, sourceTermId, sourceTerminals); var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals); if (!sourcePos || !targetPos) return; - // Add offset for multiple connections to avoid overlap var routeOffset = connIndex * 8; - // Create orthogonal path - var path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset); - var color = conn.color || self.PHASE_COLORS[conn.connection_type] || self.COLORS.connection; + var path; + if (conn.path_data) { + path = conn.path_data; + } else { + path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq); + } - // Connection shadow - html += ''; + html += ''; - // Connection line - clickable for editing html += ''; + html += ''; + html += 'fill="none" stroke="' + color + '" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>'; - // Label on the connection if (conn.output_label) { - // Calculate label position along the path var labelX = (sourcePos.x + targetPos.x) / 2; - var labelY = sourcePos.isTop ? sourcePos.y - 50 - routeOffset : sourcePos.y + 50 + routeOffset; + var labelY = sourcePos.isTop ? sourcePos.y - 70 - routeOffset : sourcePos.y + 70 + routeOffset; - var labelWidth = Math.min(conn.output_label.length * 6 + 10, 80); - html += ''; - html += ''; + var labelWidth = Math.min(conn.output_label.length * 8 + 14, 112); + html += ''; + html += ''; html += self.escapeHtml(conn.output_label); html += ''; } - // Connection type indicator if (conn.connection_type && !conn.output_label) { var typeX = (sourcePos.x + targetPos.x) / 2; var typeY = (sourcePos.y + targetPos.y) / 2; - html += ''; + html += ''; html += conn.connection_type; html += ''; } html += ''; + renderedCount++; }); + console.log('SchematicEditor renderConnections: Rendered ' + renderedCount + ' connections'); $layer.html(html); + + // Bind click events to SVG connection elements (must be done after rendering) + var self = this; + + // Normal connections + $layer.find('.schematic-connection-group').each(function() { + var $group = $(this); + var connId = $group.data('connection-id'); + var $visiblePath = $group.find('.schematic-connection'); + + this.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (connId) { + self.showConnectionPopup(connId, e.clientX, e.clientY); + } + }); + + this.addEventListener('mouseenter', function() { + $visiblePath.attr('stroke-width', '5'); + }); + this.addEventListener('mouseleave', function() { + $visiblePath.attr('stroke-width', '3.5'); + }); + this.style.cursor = 'pointer'; + }); + + // Abgang (Output) groups - click to edit + $layer.find('.schematic-output-group').each(function() { + var $group = $(this); + var connId = $group.data('connection-id'); + var $visiblePath = $group.find('.schematic-connection'); + + this.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (connId) { + self.showConnectionPopup(connId, e.clientX, e.clientY); + } + }); + + this.addEventListener('mouseenter', function() { + $visiblePath.attr('stroke-width', '5'); + }); + this.addEventListener('mouseleave', function() { + $visiblePath.attr('stroke-width', '3'); + }); + }); + + // Anschlusspunkt (Input) groups - click to edit + $layer.find('.schematic-input-group').each(function() { + var $group = $(this); + var connId = $group.data('connection-id'); + var $visiblePath = $group.find('.schematic-connection'); + + this.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (connId) { + self.showConnectionPopup(connId, e.clientX, e.clientY); + } + }); + + this.addEventListener('mouseenter', function() { + $visiblePath.attr('stroke-width', '5'); + }); + this.addEventListener('mouseleave', function() { + $visiblePath.attr('stroke-width', '3'); + }); + }); + }, + + renderControls: function() { + var self = this; + var $canvas = $('.schematic-editor-canvas'); + + // Remove existing controls + $canvas.find('.schematic-controls').remove(); + + // Find wrapper or canvas for control placement + var $wrapper = $canvas.find('.schematic-zoom-wrapper'); + var $controlsParent = $wrapper.length ? $wrapper : $canvas; + + // Create controls container (positioned absolute over SVG) + var $controls = $('
'); + + // Button style + var btnStyle = 'pointer-events:auto;width:28px;height:28px;border-radius:50%;border:2px solid #3498db;' + + 'background:#2d2d44;color:#3498db;font-size:18px;font-weight:bold;cursor:pointer;' + + 'display:flex;align-items:center;justify-content:center;transition:all 0.2s;'; + var btnHoverStyle = 'background:#3498db;color:#fff;'; + + // Add Panel button (right of last panel) + if (this.panels.length > 0) { + var lastPanel = this.panels[this.panels.length - 1]; + var addPanelX = (lastPanel._x || 0) + (lastPanel._width || 200) + 20; + var addPanelY = this.panelTopMargin + 50; + + var $addPanel = $(''); + $controls.append($addPanel); + + // Copy Panel button (if more than one panel exists) + if (this.panels.length >= 1) { + var $copyPanel = $(''); + $controls.append($copyPanel); + } + } else { + // No panels - show add panel button + var $addPanel = $(''); + $controls.append($addPanel); + } + + // Add Carrier button (below each panel's last carrier) + this.panels.forEach(function(panel) { + var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; }); + var lastCarrier = panelCarriers[panelCarriers.length - 1]; + + if (lastCarrier && lastCarrier._y) { + var addCarrierY = lastCarrier._y + self.RAIL_HEIGHT + 30; + var addCarrierX = (panel._x || 0) + 60; + + var $addCarrier = $(''); + $controls.append($addCarrier); + + // Copy Carrier button + if (panelCarriers.length >= 1) { + var $copyCarrier = $(''); + $controls.append($copyCarrier); + } + } + }); + + // Add Equipment button & Copy button (next to last equipment on each carrier) + this.carriers.forEach(function(carrier) { + var carrierEquipment = self.equipment.filter(function(e) { return String(e.carrier_id) === String(carrier.id); }); + + // Check if carrier has position data (use typeof to allow 0 values) + if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') { + // Find last equipment position + var lastPos = 0; + var lastEquipment = null; + carrierEquipment.forEach(function(eq) { + var endPos = (parseInt(eq.position_te) || 1) + (parseInt(eq.width_te) || 1) - 1; + if (endPos > lastPos) { + lastPos = endPos; + lastEquipment = eq; + } + }); + + // Calculate next free position + var nextX = carrier._x + lastPos * self.TE_WIDTH + 5; + var btnY = carrier._y - self.BLOCK_HEIGHT / 2 - 5; + + // Check if there's space left + var totalTE = parseInt(carrier.total_te) || 12; + console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): lastPos=' + lastPos + ', totalTE=' + totalTE + ', hasSpace=' + (lastPos < totalTE) + ', lastEquipment=' + (lastEquipment ? lastEquipment.id : 'none')); + + if (lastPos < totalTE) { + // Add Equipment button + var $addEquipment = $(''); + $controls.append($addEquipment); + + // Copy Equipment button (next to last equipment) + if (lastEquipment) { + // Position copy button right after the + button + var copyBtnX = nextX + 30; + var $copyEquipment = $(''); + $controls.append($copyEquipment); + } + } + + // Add Busbar button (at the left side of carrier, above the rail) + var busbarBtnX = carrier._x - 25; + var busbarBtnY = carrier._y - self.BLOCK_HEIGHT / 2 - 35; + var busbarBtnStyle = 'pointer-events:auto;width:24px;height:24px;border-radius:4px;border:2px solid #e74c3c;' + + 'background:#2d2d44;color:#e74c3c;font-size:11px;font-weight:bold;cursor:pointer;' + + 'display:flex;align-items:center;justify-content:center;transition:all 0.2s;'; + var $addBusbar = $(''); + $controls.append($addBusbar); + } else { + console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position'); + } + }); + + // Append controls to wrapper (if exists) or canvas + $canvas.css('position', 'relative'); + $controlsParent.css('position', 'relative').append($controls); + + // Bind control events + this.bindControlEvents(); + }, + + bindControlEvents: function() { + var self = this; + + // Add Panel + $(document).off('click.addPanel').on('click.addPanel', '.schematic-add-panel', function(e) { + e.preventDefault(); + self.showAddPanelDialog(); + }); + + // Copy Panel + $(document).off('click.copyPanel').on('click.copyPanel', '.schematic-copy-panel', function(e) { + e.preventDefault(); + var panelId = $(this).data('panel-id'); + self.duplicatePanel(panelId); + }); + + // Add Carrier + $(document).off('click.addCarrier').on('click.addCarrier', '.schematic-add-carrier', function(e) { + e.preventDefault(); + var panelId = $(this).data('panel-id'); + self.showAddCarrierDialog(panelId); + }); + + // Copy Carrier + $(document).off('click.copyCarrier').on('click.copyCarrier', '.schematic-copy-carrier', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.duplicateCarrier(carrierId); + }); + + // Add Equipment + $(document).off('click.addEquipment').on('click.addEquipment', '.schematic-add-equipment', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showAddEquipmentDialog(carrierId); + }); + + // Copy Equipment (single copy) + $(document).off('click.copyEquipment').on('click.copyEquipment', '.schematic-copy-equipment', function(e) { + e.preventDefault(); + var equipmentId = $(this).data('equipment-id'); + self.duplicateSingleEquipment(equipmentId); + }); + + // Add Busbar (Sammelschiene) + $(document).off('click.addBusbar').on('click.addBusbar', '.schematic-add-busbar', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showAddBusbarDialog(carrierId); + }); + + // Hover effects + $('.schematic-controls button').hover( + function() { $(this).css({ background: '#3498db', color: '#fff' }); }, + function() { $(this).css({ background: '#2d2d44', color: '#3498db' }); } + ); + + // Special hover for busbar button + $('.schematic-add-busbar').hover( + function() { $(this).css({ background: '#e74c3c', color: '#fff' }); }, + function() { $(this).css({ background: '#2d2d44', color: '#e74c3c' }); } + ); + }, + + showAddPanelDialog: function() { + var self = this; + var html = '
'; + html += '
'; + html += '

Neues Feld hinzufügen

'; + html += '
'; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.dialog-cancel, .schematic-dialog-overlay').on('click', function() { + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + + $('.dialog-save').on('click', function() { + var label = $('.dialog-panel-label').val(); + self.createPanel(label); + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + }, + + createPanel: function(label) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + method: 'POST', + data: { + action: 'create', + anlage_id: this.anlageId, + label: label, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Feld erstellt', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + duplicatePanel: function(panelId) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + method: 'POST', + data: { + action: 'duplicate', + panel_id: panelId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Feld kopiert', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + showAddCarrierDialog: function(panelId) { + var self = this; + var html = '
'; + html += '
'; + html += '

Neue Hutschiene hinzufügen

'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.dialog-cancel, .schematic-dialog-overlay').on('click', function() { + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + + $('.dialog-save').on('click', function() { + var label = $('.dialog-carrier-label').val(); + var totalTE = parseInt($('.dialog-carrier-te').val()) || 12; + self.createCarrier(panelId, label, totalTE); + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + }, + + createCarrier: function(panelId, label, totalTE) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + method: 'POST', + data: { + action: 'create', + anlage_id: this.anlageId, + panel_id: panelId, + label: label, + total_te: totalTE, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Hutschiene erstellt', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + duplicateCarrier: function(carrierId) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + method: 'POST', + data: { + action: 'duplicate', + carrier_id: carrierId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Hutschiene kopiert', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + showAddBusbarDialog: function(carrierId) { + var self = this; + var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); }); + var totalTE = carrier ? (parseInt(carrier.total_te) || 12) : 12; + + var html = '
'; + html += '
'; + html += '

Sammelschiene hinzufügen

'; + + // Phase type selection + html += '
'; + html += '
'; + + // Start TE + html += '
'; + html += '
'; + + // End TE + html += '
'; + html += '
'; + + // Position (above/below) + html += '
'; + html += '
'; + + // Excluded TEs + html += '
'; + html += '
'; + + // Color + html += '
'; + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + // Auto-set color based on phase selection + $('.dialog-busbar-phases').on('change', function() { + var phase = $(this).val(); + var color = '#e74c3c'; // default red + if (phase === 'L1') color = '#8B4513'; // brown + else if (phase === 'L2') color = '#000000'; // black + else if (phase === 'L3') color = '#808080'; // gray + else if (phase === 'N') color = '#3498db'; // blue + else if (phase === 'PE') color = '#f1c40f'; // yellow-green + else if (phase === 'L1L2L3') color = '#e74c3c'; // red for 3-phase + $('.dialog-busbar-color').val(color); + }); + + $('.dialog-cancel, .schematic-dialog-overlay').on('click', function() { + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + + $('.dialog-save').on('click', function() { + var phases = $('.dialog-busbar-phases').val(); + var startTE = parseInt($('.dialog-busbar-start').val()) || 1; + var endTE = parseInt($('.dialog-busbar-end').val()) || totalTE; + var positionY = parseInt($('.dialog-busbar-position').val()) || 0; + var color = $('.dialog-busbar-color').val(); + var excludedTE = $('.dialog-busbar-excluded').val() || ''; + + self.createBusbar(carrierId, phases, startTE, endTE, positionY, color, excludedTE); + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + }, + + createBusbar: function(carrierId, phases, startTE, endTE, positionY, color, excludedTE) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'create_rail', + carrier_id: carrierId, + rail_start_te: startTE, + rail_end_te: endTE, + rail_phases: phases, + position_y: positionY, + color: color, + excluded_te: excludedTE, + connection_type: phases, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Sammelschiene erstellt', 'success'); + // Add to local connections and re-render + self.connections.push({ + id: response.connection_id, + is_rail: 1, + fk_carrier: carrierId, + rail_start_te: startTE, + rail_end_te: endTE, + rail_phases: phases, + position_y: positionY, + color: color, + excluded_te: excludedTE, + connection_type: phases + }); + self.render(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + showAddEquipmentDialog: function(carrierId) { + var self = this; + + // Load equipment types first + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'get_types', system_id: 1 }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showEquipmentTypeSelector(carrierId, response.types); + } + } + }); + }, + + showEquipmentTypeSelector: function(carrierId, types) { + var self = this; + var html = '
'; + html += '
'; + html += '

Equipment hinzufügen

'; + + html += '
'; + html += '
'; + + html += '
'; + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.dialog-cancel, .schematic-dialog-overlay').on('click', function() { + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + + $('.dialog-save').on('click', function() { + var typeId = $('.dialog-equipment-type').val(); + var label = $('.dialog-equipment-label').val(); + self.createEquipment(carrierId, typeId, label); + $('.schematic-dialog, .schematic-dialog-overlay').remove(); + }); + }, + + createEquipment: function(carrierId, typeId, label) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'create', + carrier_id: carrierId, + type_id: typeId, + label: label, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Equipment erstellt', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + + duplicateSingleEquipment: function(equipmentId) { + var self = this; + + // Find original equipment to get carrier info + var originalEq = this.equipment.find(function(e) { return String(e.id) === String(equipmentId); }); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'duplicate', + equipment_id: equipmentId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success && response.equipment) { + self.showMessage('Equipment kopiert', 'success'); + // Add new equipment to local array instead of reloading everything + var newEq = response.equipment; + newEq.carrier_id = originalEq ? originalEq.carrier_id : newEq.fk_carrier; + newEq.panel_id = originalEq ? originalEq.panel_id : null; + self.equipment.push(newEq); + self.render(); + } else if (response.success) { + // Fallback if no equipment data returned + self.showMessage('Equipment kopiert', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Kein Platz mehr', 'warning'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + + // Check if an equipment position is covered by a busbar + isEquipmentCoveredByBusbar: function(eq) { + var eqCarrierId = eq.carrier_id || eq.fk_carrier; + var eqPosTE = parseInt(eq.position_te) || 1; + var eqWidthTE = parseInt(eq.width_te) || 1; + var eqEndTE = eqPosTE + eqWidthTE - 1; + + // Check all busbars (is_rail connections) that are ABOVE the blocks (position_y=0) + var covered = this.connections.some(function(conn) { + // Must be a rail/busbar + if (!conn.is_rail || conn.is_rail === '0' || conn.is_rail === 0) return false; + if (parseInt(conn.is_rail) !== 1) return false; + + // Must be above the blocks (position_y = 0) + if (parseInt(conn.position_y) !== 0) return false; + + // Busbar must be on same carrier + if (String(conn.fk_carrier) !== String(eqCarrierId)) return false; + + var railStart = parseInt(conn.rail_start_te) || 1; + var railEnd = parseInt(conn.rail_end_te) || railStart; + + // Check if equipment overlaps with busbar range + // Equipment is covered if its start OR end is within the busbar range + var overlaps = !(eqEndTE < railStart || eqPosTE > railEnd); + return overlaps; + }); + + return covered; }, getTerminals: function(eq) { @@ -4893,6 +6419,12 @@ var termIndex = 0; var samePosList = []; + // Map legacy terminal names to positions + var legacyMap = { + 'in': 'top', 'input': 'top', 'in_L': 'top', 'in_N': 'top', + 'out': 'bottom', 'output': 'bottom', 'out_L': 'bottom', 'out_N': 'bottom' + }; + for (var i = 0; i < terminals.length; i++) { if (terminals[i].id === terminalId) { terminal = terminals[i]; @@ -4900,152 +6432,750 @@ } } + // If not found by exact match, try legacy mapping + if (!terminal && legacyMap[terminalId]) { + var targetPos = legacyMap[terminalId]; + for (var k = 0; k < terminals.length; k++) { + if (terminals[k].pos === targetPos) { + terminal = terminals[k]; + break; + } + } + } + if (!terminal) { - terminal = terminals[0] || { pos: 'top' }; + // Default: use first bottom terminal for 'out', first top for anything else + var defaultPos = (terminalId && (terminalId.indexOf('out') !== -1 || terminalId === 't2')) ? 'bottom' : 'top'; + for (var m = 0; m < terminals.length; m++) { + if (terminals[m].pos === defaultPos) { + terminal = terminals[m]; + break; + } + } + if (!terminal) { + terminal = terminals[0] || { pos: 'top' }; + } } // Get all terminals with same position for (var j = 0; j < terminals.length; j++) { if (terminals[j].pos === terminal.pos) { - if (terminals[j].id === terminalId) termIndex = samePosList.length; + if (terminals[j].id === terminal.id) termIndex = samePosList.length; samePosList.push(terminals[j]); } } var isTop = terminal.pos === 'top'; - var spacing = eq._width / (samePosList.length + 1); - var x = eq._x + spacing * (termIndex + 1); + var widthTE = parseInt(eq.width_te) || 1; + + // Terminal im festen TE-Raster platzieren + // Jeder Terminal belegt 1 TE - Index bestimmt welches TE + var teIndex = termIndex % widthTE; + var x = eq._x + (teIndex * this.TE_WIDTH) + (this.TE_WIDTH / 2); var y = isTop ? (eq._y - 5) : (eq._y + eq._height + 5); return { x: x, y: y, isTop: isTop }; }, - createOrthogonalPath: function(source, target, routeOffset) { + createOrthogonalPath: function(source, target, routeOffset, sourceEq, targetEq) { var x1 = source.x, y1 = source.y; var x2 = target.x, y2 = target.y; - routeOffset = routeOffset || 0; - // Constants for routing around blocks - var MARGIN = 25 + routeOffset; // Distance to keep from blocks - var VERTICAL_OFFSET = 40 + routeOffset; // How far to route around + // Connection index for spreading + var connIndex = Math.floor(routeOffset / 8); - // Both on top - route above - if (source.isTop && target.isTop) { - var routeY = Math.min(y1, y2) - VERTICAL_OFFSET; + // Use pathfinding with obstacle avoidance + if (typeof PF !== 'undefined') { + try { + var path = this.createPathfindingRoute(x1, y1, x2, y2, connIndex); + if (path) return path; + } catch (e) { + console.error('Pathfinding error:', e); + } + } + + // Fallback to simple route + return this.createSimpleRoute(x1, y1, x2, y2, source.isTop, target.isTop, routeOffset); + }, + + // Pathfinding-based routing with obstacle avoidance and connection spreading + createPathfindingRoute: function(x1, y1, x2, y2, connIndex) { + var self = this; + var GRID_SIZE = 5; + var PADDING = 60; + var BLOCK_MARGIN = 2; + var WIRE_SPREAD = 10; // Pixels between parallel wires + + // Apply offset to spread parallel wires apart + var spreadOffset = connIndex * WIRE_SPREAD; + // Alternate left/right to spread evenly + var spreadDir = (connIndex % 2 === 0) ? 1 : -1; + var xSpread = spreadDir * Math.floor(connIndex / 2) * WIRE_SPREAD; + + // Find bounds of all equipment + var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + this.equipment.forEach(function(eq) { + if (eq._x !== undefined && eq._y !== undefined) { + minX = Math.min(minX, eq._x); + maxX = Math.max(maxX, eq._x + (eq._width || self.TE_WIDTH * eq.width_te)); + minY = Math.min(minY, eq._y); + maxY = Math.max(maxY, eq._y + (eq._height || self.BLOCK_HEIGHT)); + } + }); + + // Include start and end points with padding + minX = Math.min(minX, x1, x2) - PADDING; + maxX = Math.max(maxX, x1, x2) + PADDING; + minY = Math.min(minY, y1, y2) - PADDING; + maxY = Math.max(maxY, y1, y2) + PADDING; + + // Create grid + var gridWidth = Math.ceil((maxX - minX) / GRID_SIZE); + var gridHeight = Math.ceil((maxY - minY) / GRID_SIZE); + + // Limit grid size for performance + if (gridWidth > 300) gridWidth = 300; + if (gridHeight > 300) gridHeight = 300; + + var grid = new PF.Grid(gridWidth, gridHeight); + + // Mark ALL equipment as obstacles (unwalkable) - including source and target blocks + this.equipment.forEach(function(eq) { + if (eq._x === undefined || eq._y === undefined) return; + + var eqWidth = eq._width || (self.TE_WIDTH * (parseInt(eq.width_te) || 1)); + var eqHeight = eq._height || self.BLOCK_HEIGHT; + + var eqX1 = Math.floor((eq._x - minX) / GRID_SIZE); + var eqY1 = Math.floor((eq._y - minY) / GRID_SIZE); + var eqX2 = Math.ceil((eq._x + eqWidth - minX) / GRID_SIZE); + var eqY2 = Math.ceil((eq._y + eqHeight - minY) / GRID_SIZE); + + // Add margin around blocks to keep lines away + eqX1 = Math.max(0, eqX1 - BLOCK_MARGIN); + eqY1 = Math.max(0, eqY1 - BLOCK_MARGIN); + eqX2 = Math.min(gridWidth - 1, eqX2 + BLOCK_MARGIN); + eqY2 = Math.min(gridHeight - 1, eqY2 + BLOCK_MARGIN); + + for (var gx = eqX1; gx <= eqX2; gx++) { + for (var gy = eqY1; gy <= eqY2; gy++) { + if (gx >= 0 && gx < gridWidth && gy >= 0 && gy < gridHeight) { + grid.setWalkableAt(gx, gy, false); + } + } + } + }); + + // Mark busbars/rails as obstacles too (Phasenschienen) + this.connections.forEach(function(conn) { + if (parseInt(conn.is_rail) !== 1) return; + + // Find the carrier for this busbar + var carrier = self.carriers.find(function(c) { + return String(c.id) === String(conn.fk_carrier); + }); + if (!carrier || carrier._x === undefined) return; + + var startTE = parseInt(conn.rail_start_te) || 1; + var endTE = parseInt(conn.rail_end_te) || 12; + + // Calculate busbar position + var busbarX = carrier._x + (startTE - 1) * self.TE_WIDTH; + var busbarWidth = (endTE - startTE + 1) * self.TE_WIDTH; + var busbarY = carrier._y + self.BLOCK_HEIGHT + 15; // Position below blocks + var busbarHeight = 20; // Approximate busbar height + + var bbX1 = Math.floor((busbarX - minX) / GRID_SIZE); + var bbY1 = Math.floor((busbarY - minY) / GRID_SIZE); + var bbX2 = Math.ceil((busbarX + busbarWidth - minX) / GRID_SIZE); + var bbY2 = Math.ceil((busbarY + busbarHeight - minY) / GRID_SIZE); + + // Add margin around busbars + bbX1 = Math.max(0, bbX1 - BLOCK_MARGIN); + bbY1 = Math.max(0, bbY1 - BLOCK_MARGIN); + bbX2 = Math.min(gridWidth - 1, bbX2 + BLOCK_MARGIN); + bbY2 = Math.min(gridHeight - 1, bbY2 + BLOCK_MARGIN); + + for (var bx = bbX1; bx <= bbX2; bx++) { + for (var by = bbY1; by <= bbY2; by++) { + if (bx >= 0 && bx < gridWidth && by >= 0 && by < gridHeight) { + grid.setWalkableAt(bx, by, false); + } + } + } + + // Also mark the CENTER LABEL of the busbar as obstacle + // Label is in the center of the busbar + var phases = conn.rail_phases || conn.connection_type || ''; + if (phases) { + var labelWidth = phases.length * 10 + 10; // Approximate text width + padding + var labelHeight = 16; // Font size + padding + var labelX = busbarX + busbarWidth / 2 - labelWidth / 2; + var labelY = busbarY - 5; // Label is on top of busbar + + var lblX1 = Math.floor((labelX - minX) / GRID_SIZE); + var lblY1 = Math.floor((labelY - minY) / GRID_SIZE); + var lblX2 = Math.ceil((labelX + labelWidth - minX) / GRID_SIZE); + var lblY2 = Math.ceil((labelY + labelHeight - minY) / GRID_SIZE); + + // Add margin around label + lblX1 = Math.max(0, lblX1 - 1); + lblY1 = Math.max(0, lblY1 - 1); + lblX2 = Math.min(gridWidth - 1, lblX2 + 1); + lblY2 = Math.min(gridHeight - 1, lblY2 + 1); + + for (var lx = lblX1; lx <= lblX2; lx++) { + for (var ly = lblY1; ly <= lblY2; ly++) { + if (lx >= 0 && lx < gridWidth && ly >= 0 && ly < gridHeight) { + grid.setWalkableAt(lx, ly, false); + } + } + } + } + }); + + // Apply horizontal spread to separate parallel wires + var x1Spread = x1 + xSpread; + var x2Spread = x2 + xSpread; + + // Convert pixel coords to grid coords + var startX = Math.round((x1Spread - minX) / GRID_SIZE); + var startY = Math.round((y1 - minY) / GRID_SIZE); + var endX = Math.round((x2Spread - minX) / GRID_SIZE); + var endY = Math.round((y2 - minY) / GRID_SIZE); + + // Clamp to grid bounds + startX = Math.max(0, Math.min(gridWidth - 1, startX)); + startY = Math.max(0, Math.min(gridHeight - 1, startY)); + endX = Math.max(0, Math.min(gridWidth - 1, endX)); + endY = Math.max(0, Math.min(gridHeight - 1, endY)); + + // Make sure start and end points and their immediate surroundings are walkable + // This creates a "corridor" from the terminal into the routing space + for (var dx = -3; dx <= 3; dx++) { + for (var dy = -3; dy <= 3; dy++) { + var sx = startX + dx; + var sy = startY + dy; + var ex = endX + dx; + var ey = endY + dy; + if (sx >= 0 && sx < gridWidth && sy >= 0 && sy < gridHeight) { + grid.setWalkableAt(sx, sy, true); + } + if (ex >= 0 && ex < gridWidth && ey >= 0 && ey < gridHeight) { + grid.setWalkableAt(ex, ey, true); + } + } + } + + // Find path using JumpPoint finder - produces cleaner orthogonal paths + var finder = new PF.JumpPointFinder({ + allowDiagonal: false + }); + + var path = finder.findPath(startX, startY, endX, endY, grid.clone()); + + // If no path found, use simple fallback + if (!path || path.length === 0) { + return this.createSimpleRoute(x1, y1, x2, y2, true, true, routeOffset); + } + + // Convert path to orthogonal (only horizontal and vertical segments) + // This eliminates the "snake" effect by keeping only direction changes + var orthogonalPath = this.makeOrthogonal(path); + + // Convert grid path back to pixel coordinates + var pixelPath = []; + for (var i = 0; i < orthogonalPath.length; i++) { + pixelPath.push({ + x: minX + orthogonalPath[i][0] * GRID_SIZE, + y: minY + orthogonalPath[i][1] * GRID_SIZE + }); + } + + // Build SVG path starting from exact source position + var svgPath = 'M ' + x1 + ' ' + y1; + + // Add intermediate waypoints (skip first if too close to start) + for (var j = 0; j < pixelPath.length; j++) { + svgPath += ' L ' + pixelPath[j].x + ' ' + pixelPath[j].y; + } + + // End at exact target position + svgPath += ' L ' + x2 + ' ' + y2; + + return svgPath; + }, + + // Simplify path to only keep corner points (where direction changes) + makeOrthogonal: function(path) { + if (path.length < 3) return path; + + var result = [path[0]]; // Always keep start + var prevDir = null; + + for (var i = 1; i < path.length; i++) { + var dx = path[i][0] - path[i-1][0]; + var dy = path[i][1] - path[i-1][1]; + + // Determine direction: 'h' for horizontal, 'v' for vertical + var dir = (dx !== 0) ? 'h' : 'v'; + + // If direction changed, add the previous point as a corner + if (prevDir !== null && dir !== prevDir) { + result.push(path[i-1]); + } + + prevDir = dir; + } + + // Always keep end point + result.push(path[path.length - 1]); + + return result; + }, + + // Simple fallback routing + createSimpleRoute: function(x1, y1, x2, y2, sourceIsTop, targetIsTop, routeOffset) { + var MARGIN = 25 + (routeOffset || 0); + + // Simple L-shaped or U-shaped route + if (sourceIsTop && targetIsTop) { + var routeY = Math.min(y1, y2) - MARGIN; return 'M ' + x1 + ' ' + y1 + ' L ' + x1 + ' ' + routeY + ' L ' + x2 + ' ' + routeY + ' L ' + x2 + ' ' + y2; - } - - // Both on bottom - route below - if (!source.isTop && !target.isTop) { - var routeYBottom = Math.max(y1, y2) + VERTICAL_OFFSET; + } else if (!sourceIsTop && !targetIsTop) { + var routeYBot = Math.max(y1, y2) + MARGIN; return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + routeYBottom + - ' L ' + x2 + ' ' + routeYBottom + + ' L ' + x1 + ' ' + routeYBot + + ' L ' + x2 + ' ' + routeYBot + + ' L ' + x2 + ' ' + y2; + } else { + // Mixed - route through middle + var midY = (y1 + y2) / 2; + return 'M ' + x1 + ' ' + y1 + + ' L ' + x1 + ' ' + midY + + ' L ' + x2 + ' ' + midY + ' L ' + x2 + ' ' + y2; } - - // One top, one bottom - route around the side - var dx = x2 - x1; - var dy = y2 - y1; - - // If vertically aligned, go straight or with small offset - if (Math.abs(dx) < 20) { - return 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2; - } - - // Route to the side, then up/down, then to target - // Choose side based on relative position - var sideX = dx > 0 ? x1 + MARGIN : x1 - MARGIN; - - // Top to bottom - if (source.isTop && !target.isTop) { - // Go up from source, across, down to target - var upY = y1 - VERTICAL_OFFSET; - var downY = y2 + VERTICAL_OFFSET; - - if (Math.abs(y1 - y2) > 100) { - // Far apart - route around the shorter side - return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + upY + - ' L ' + x2 + ' ' + upY + - ' L ' + x2 + ' ' + y2; - } else { - // Close - route around - var midX = (x1 + x2) / 2 + (dx > 0 ? -MARGIN : MARGIN); - return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + upY + - ' L ' + midX + ' ' + upY + - ' L ' + midX + ' ' + downY + - ' L ' + x2 + ' ' + downY + - ' L ' + x2 + ' ' + y2; - } - } - - // Bottom to top - if (!source.isTop && target.isTop) { - var sourceDownY = y1 + VERTICAL_OFFSET; - var targetUpY = y2 - VERTICAL_OFFSET; - - if (Math.abs(y1 - y2) > 100) { - return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + sourceDownY + - ' L ' + x2 + ' ' + sourceDownY + - ' L ' + x2 + ' ' + y2; - } else { - var midX2 = (x1 + x2) / 2 + (dx > 0 ? -MARGIN : MARGIN); - return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + sourceDownY + - ' L ' + midX2 + ' ' + sourceDownY + - ' L ' + midX2 + ' ' + targetUpY + - ' L ' + x2 + ' ' + targetUpY + - ' L ' + x2 + ' ' + y2; - } - } - - // Fallback - simple orthogonal - var midY = (y1 + y2) / 2; - return 'M ' + x1 + ' ' + y1 + - ' L ' + x1 + ' ' + midY + - ' L ' + x2 + ' ' + midY + - ' L ' + x2 + ' ' + y2; }, + // Handle terminal click - only in wire draw mode handleTerminalClick: function($terminal) { var eqId = $terminal.data('equipment-id'); var termId = $terminal.data('terminal-id'); - if (!this.selectedTerminal) { - // First click - select any terminal - this.selectedTerminal = { - element: $terminal, - equipmentId: eqId, - terminalId: termId - }; + // Only works in manual wire draw mode + if (!this.wireDrawMode) return; - $terminal.find('.schematic-terminal-circle').attr('stroke', this.COLORS.selected).attr('stroke-width', '3'); - $('.schematic-connection-preview').show(); + if (!this.wireDrawSourceEq) { + // First terminal - start drawing + this.wireDrawSourceEq = eqId; + this.wireDrawSourceTerm = termId; + this.wireDrawPoints = []; - this.showMessage('Jetzt Ziel-Terminal auswählen...', 'info'); - } else { - // Second click - check if different equipment - if (eqId === this.selectedTerminal.equipmentId) { - this.showMessage('Bitte ein anderes Gerät auswählen', 'warning'); - return; + // Get terminal position as first point + var eq = this.equipment.find(function(e) { return String(e.id) === String(eqId); }); + if (eq) { + var terminals = this.getTerminals(eq); + var termPos = this.getTerminalPosition(eq, termId, terminals); + if (termPos) { + this.wireDrawPoints.push({x: termPos.x, y: termPos.y}); + } } - // Show label dialog before creating connection - this.showConnectionLabelDialog( - this.selectedTerminal.equipmentId, - this.selectedTerminal.terminalId, - eqId, - termId - ); + $terminal.find('.schematic-terminal-circle').attr('stroke', '#ff0').attr('stroke-width', '3'); + this.showMessage('Rasterpunkte klicken, Rechtsklick = Abbruch, dann Ziel-Terminal klicken', 'info'); + this.showWireGrid(); + } else if (eqId !== this.wireDrawSourceEq) { + // Second terminal - finish and save + this.finishWireDrawing(eqId, termId); } }, + // Wire draw mode functions + toggleWireDrawMode: function() { + this.wireDrawMode = !this.wireDrawMode; + var $btn = $('.schematic-wire-draw-toggle'); + + if (this.wireDrawMode) { + $btn.addClass('active').css('background', '#27ae60'); + this.showMessage('Manueller Zeichenmodus: Klicken Sie auf ein START-Terminal (roter Kreis)', 'info'); + this.showWireGrid(); + // Highlight all terminals to show they are clickable + $(this.svgElement).find('.schematic-terminal-circle').css('cursor', 'crosshair'); + } else { + $btn.removeClass('active').css('background', ''); + this.cancelWireDrawing(); + this.hideWireGrid(); + this.showMessage('Automatischer Modus aktiviert', 'info'); + } + }, + + showWireGrid: function() { + console.log('showWireGrid called, svgElement:', this.svgElement); + if (!this.svgElement) { + console.error('No SVG element found!'); + return; + } + + // Remove existing wire grid + var existing = this.svgElement.querySelector('.schematic-wire-grid-layer'); + if (existing) existing.remove(); + + var self = this; + var svgNS = 'http://www.w3.org/2000/svg'; + var gridLayer = document.createElementNS(svgNS, 'g'); + gridLayer.setAttribute('class', 'schematic-wire-grid-layer'); + gridLayer.setAttribute('pointer-events', 'none'); + + // Collect all terminal positions and routing Y-lines + this.wireGridPoints = []; + var pointCount = 0; + + // Get X positions from all TE slots on all carriers + var xPositions = []; + this.carriers.forEach(function(carrier) { + if (typeof carrier._x === 'undefined') return; + var totalTE = carrier.total_te || 12; + for (var te = 1; te <= totalTE; te++) { + // Terminal center X position for this TE + var teX = carrier._x + (te - 1) * self.TE_WIDTH + (self.TE_WIDTH / 2); + if (xPositions.indexOf(teX) === -1) xPositions.push(teX); + } + }); + + // Get Y positions from equipment terminals and routing space + var yPositions = []; + this.equipment.forEach(function(eq) { + if (typeof eq._y === 'undefined') return; + // Top terminal Y + var topY = eq._y - 7; + // Bottom terminal Y + var bottomY = eq._y + (eq._height || self.BLOCK_HEIGHT) + 7; + if (yPositions.indexOf(topY) === -1) yPositions.push(topY); + if (yPositions.indexOf(bottomY) === -1) yPositions.push(bottomY); + }); + + // Add routing Y lines between carriers (for horizontal routing) + this.carriers.forEach(function(carrier) { + if (typeof carrier._y === 'undefined') return; + var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; + var blockTop = railCenterY - self.BLOCK_HEIGHT / 2; + var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2; + + // Add grid lines above and below the busbar area + var busbarSpace = 60; // Space for busbars + for (var offsetY = busbarSpace + 20; offsetY < busbarSpace + 100; offsetY += self.WIRE_GRID_SIZE) { + var routeY = blockTop - offsetY; + if (routeY > 0 && yPositions.indexOf(routeY) === -1) yPositions.push(routeY); + } + for (var offsetY2 = busbarSpace + 20; offsetY2 < busbarSpace + 100; offsetY2 += self.WIRE_GRID_SIZE) { + var routeY2 = blockBottom + offsetY2; + if (yPositions.indexOf(routeY2) === -1) yPositions.push(routeY2); + } + }); + + // Sort positions + xPositions.sort(function(a, b) { return a - b; }); + yPositions.sort(function(a, b) { return a - b; }); + + // Draw grid points at terminal-aligned positions + xPositions.forEach(function(x) { + yPositions.forEach(function(y) { + var circle = document.createElementNS(svgNS, 'circle'); + circle.setAttribute('cx', x); + circle.setAttribute('cy', y); + circle.setAttribute('r', '1.5'); + circle.setAttribute('fill', '#888'); + circle.setAttribute('opacity', '0.4'); + gridLayer.appendChild(circle); + self.wireGridPoints.push({x: x, y: y}); + pointCount++; + }); + }); + + console.log('Wire grid: ' + xPositions.length + ' x-positions, ' + yPositions.length + ' y-positions'); + + // Create magnetic cursor indicator (shows nearest grid point) + var magnetCursor = document.createElementNS(svgNS, 'circle'); + magnetCursor.setAttribute('class', 'wire-grid-magnet'); + magnetCursor.setAttribute('r', '6'); + magnetCursor.setAttribute('fill', 'none'); + magnetCursor.setAttribute('stroke', '#27ae60'); + magnetCursor.setAttribute('stroke-width', '2'); + magnetCursor.setAttribute('opacity', '0'); + magnetCursor.setAttribute('pointer-events', 'none'); + gridLayer.appendChild(magnetCursor); + + // Append grid layer at the end of SVG (on top of everything) + this.svgElement.appendChild(gridLayer); + console.log('Wire grid created with ' + pointCount + ' points'); + + // Setup magnetic snap behavior + this.setupMagneticSnap(); + }, + + setupMagneticSnap: function() { + var self = this; + var $svg = $(this.svgElement); + var magnetRadius = 20; // Distance in pixels to activate magnetic snap + + // Remove old handler if exists + $svg.off('mousemove.magneticSnap'); + + $svg.on('mousemove.magneticSnap', function(e) { + if (!self.wireDrawMode) return; + + var rect = self.svgElement.getBoundingClientRect(); + var x = e.clientX - rect.left; + var y = e.clientY - rect.top; + + // Find nearest grid point from terminal-aligned points + var nearestX = x, nearestY = y; + var minDist = Infinity; + + if (self.wireGridPoints && self.wireGridPoints.length > 0) { + self.wireGridPoints.forEach(function(pt) { + var d = Math.sqrt(Math.pow(x - pt.x, 2) + Math.pow(y - pt.y, 2)); + if (d < minDist) { + minDist = d; + nearestX = pt.x; + nearestY = pt.y; + } + }); + } + + var magnetCursor = self.svgElement.querySelector('.wire-grid-magnet'); + if (magnetCursor) { + if (minDist <= magnetRadius) { + // Show magnetic indicator at nearest grid point + magnetCursor.setAttribute('cx', nearestX); + magnetCursor.setAttribute('cy', nearestY); + magnetCursor.setAttribute('opacity', '1'); + + // Store snapped position for click handling + self.magnetSnappedPos = {x: nearestX, y: nearestY}; + } else { + magnetCursor.setAttribute('opacity', '0'); + self.magnetSnappedPos = null; + } + } + }); + }, + + // Snap coordinates to nearest terminal-aligned grid point + snapToTerminalGrid: function(x, y) { + var self = this; + + // If we have pre-calculated grid points, find nearest + if (this.wireGridPoints && this.wireGridPoints.length > 0) { + var nearestX = x, nearestY = y; + var minDist = Infinity; + + this.wireGridPoints.forEach(function(pt) { + var d = Math.sqrt(Math.pow(x - pt.x, 2) + Math.pow(y - pt.y, 2)); + if (d < minDist) { + minDist = d; + nearestX = pt.x; + nearestY = pt.y; + } + }); + + return {x: nearestX, y: nearestY}; + } + + // Fallback: snap to TE centers for X, and nearest terminal Y + var nearestX = x, nearestY = y; + + // Find nearest TE center X + var minDistX = Infinity; + this.carriers.forEach(function(carrier) { + if (typeof carrier._x === 'undefined') return; + var totalTE = carrier.total_te || 12; + for (var te = 1; te <= totalTE; te++) { + var teX = carrier._x + (te - 1) * self.TE_WIDTH + (self.TE_WIDTH / 2); + var dx = Math.abs(x - teX); + if (dx < minDistX) { + minDistX = dx; + nearestX = teX; + } + } + }); + + // Find nearest terminal Y + var minDistY = Infinity; + this.equipment.forEach(function(eq) { + if (typeof eq._y === 'undefined') return; + var topY = eq._y - 7; + var bottomY = eq._y + (eq._height || self.BLOCK_HEIGHT) + 7; + + var dyTop = Math.abs(y - topY); + var dyBottom = Math.abs(y - bottomY); + + if (dyTop < minDistY) { + minDistY = dyTop; + nearestY = topY; + } + if (dyBottom < minDistY) { + minDistY = dyBottom; + nearestY = bottomY; + } + }); + + return {x: nearestX, y: nearestY}; + }, + + hideWireGrid: function() { + if (this.svgElement) { + var grid = this.svgElement.querySelector('.schematic-wire-grid-layer'); + if (grid) grid.remove(); + } + // Remove magnetic snap handler + $(this.svgElement).off('mousemove.magneticSnap'); + this.magnetSnappedPos = null; + this.wireGridPoints = null; + console.log('Wire grid hidden'); + }, + + updateWirePreview: function() { + var svgNS = 'http://www.w3.org/2000/svg'; + var preview = this.svgElement.querySelector('.wire-draw-preview'); + + if (!preview) { + // Create preview path using native DOM (jQuery doesn't work with SVG namespaces) + preview = document.createElementNS(svgNS, 'path'); + preview.setAttribute('class', 'wire-draw-preview'); + preview.setAttribute('fill', 'none'); + preview.setAttribute('stroke', '#27ae60'); + preview.setAttribute('stroke-width', '3'); + this.svgElement.appendChild(preview); // Add to end of SVG (on top) + } + + if (this.wireDrawPoints.length < 1) { + preview.setAttribute('d', ''); + return; + } + + // Build path from all points + var d = 'M ' + this.wireDrawPoints[0].x + ' ' + this.wireDrawPoints[0].y; + for (var i = 1; i < this.wireDrawPoints.length; i++) { + d += ' L ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y; + } + preview.setAttribute('d', d); + console.log('Wire preview updated:', d); + }, + + updateWirePreviewCursor: function(x, y) { + var svgNS = 'http://www.w3.org/2000/svg'; + var cursor = this.svgElement.querySelector('.wire-draw-cursor'); + var cursorDot = this.svgElement.querySelector('.wire-draw-cursor-dot'); + var cursorLine = this.svgElement.querySelector('.wire-draw-cursor-line'); + + if (!cursor) { + // Create cursor elements using native DOM + cursor = document.createElementNS(svgNS, 'circle'); + cursor.setAttribute('class', 'wire-draw-cursor'); + cursor.setAttribute('r', '8'); + cursor.setAttribute('fill', 'none'); + cursor.setAttribute('stroke', '#27ae60'); + cursor.setAttribute('stroke-width', '2'); + this.svgElement.appendChild(cursor); + + cursorDot = document.createElementNS(svgNS, 'circle'); + cursorDot.setAttribute('class', 'wire-draw-cursor-dot'); + cursorDot.setAttribute('r', '3'); + cursorDot.setAttribute('fill', '#27ae60'); + this.svgElement.appendChild(cursorDot); + + cursorLine = document.createElementNS(svgNS, 'line'); + cursorLine.setAttribute('class', 'wire-draw-cursor-line'); + cursorLine.setAttribute('stroke', '#27ae60'); + cursorLine.setAttribute('stroke-width', '2'); + cursorLine.setAttribute('stroke-dasharray', '5,5'); + cursorLine.setAttribute('opacity', '0.7'); + this.svgElement.appendChild(cursorLine); + } + + // Update cursor position (green snap indicator) + cursor.setAttribute('cx', x); + cursor.setAttribute('cy', y); + cursorDot.setAttribute('cx', x); + cursorDot.setAttribute('cy', y); + + // Draw line from last point to cursor (if we have points) + if (this.wireDrawPoints.length > 0) { + var lastPt = this.wireDrawPoints[this.wireDrawPoints.length - 1]; + cursorLine.setAttribute('x1', lastPt.x); + cursorLine.setAttribute('y1', lastPt.y); + cursorLine.setAttribute('x2', x); + cursorLine.setAttribute('y2', y); + cursorLine.style.display = ''; + } else { + // No points yet - hide line + cursorLine.style.display = 'none'; + } + }, + + cancelWireDrawing: function() { + this.wireDrawSourceEq = null; + this.wireDrawSourceTerm = null; + this.wireDrawPoints = []; + + // Remove all preview elements using native DOM + var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line'); + elements.forEach(function(el) { el.remove(); }); + + // Reset terminal highlight + $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', ''); + + this.showMessage('Zeichnung abgebrochen', 'info'); + }, + + finishWireDrawing: function(targetEqId, targetTermId) { + var self = this; + + // Get target terminal position + var eq = this.equipment.find(function(e) { return String(e.id) === String(targetEqId); }); + if (eq) { + var terminals = this.getTerminals(eq); + var termPos = this.getTerminalPosition(eq, targetTermId, terminals); + if (termPos) { + this.wireDrawPoints.push({x: termPos.x, y: termPos.y}); + } + } + + // Build path string from points + var pathData = ''; + for (var i = 0; i < this.wireDrawPoints.length; i++) { + pathData += (i === 0 ? 'M' : 'L') + ' ' + this.wireDrawPoints[i].x + ' ' + this.wireDrawPoints[i].y + ' '; + } + + // Show connection dialog to set labels and save + this.showConnectionLabelDialogWithPath( + this.wireDrawSourceEq, + this.wireDrawSourceTerm, + targetEqId, + targetTermId, + pathData.trim() + ); + + // Cleanup + this.wireDrawSourceEq = null; + this.wireDrawSourceTerm = null; + this.wireDrawPoints = []; + var elements = this.svgElement.querySelectorAll('.wire-draw-preview, .wire-draw-cursor, .wire-draw-cursor-dot, .wire-draw-cursor-line'); + elements.forEach(function(el) { el.remove(); }); + $(this.svgElement).find('.schematic-terminal-circle').attr('stroke', '#fff').attr('stroke-width', '2').css('cursor', ''); + }, + + showConnectionLabelDialogWithPath: function(sourceEqId, sourceTermId, targetEqId, targetTermId, pathData) { + // Store path data for later use when creating connection + this._pendingPathData = pathData; + this.showConnectionLabelDialog(sourceEqId, sourceTermId, targetEqId, targetTermId); + }, + showConnectionLabelDialog: function(sourceEqId, sourceTermId, targetEqId, targetTermId) { var self = this; @@ -5119,12 +7249,17 @@ var medium = $('#conn-medium').val(); var length = $('#conn-length').val(); + // Check if there's a manually drawn path + var pathData = self._pendingPathData || null; + self._pendingPathData = null; + self.createConnection(sourceEqId, sourceTermId, targetEqId, targetTermId, { label: label, type: connType, color: color, medium: medium, - length: length + length: length, + pathData: pathData }); $('#schematic-conn-dialog').remove(); @@ -5195,6 +7330,7 @@ output_label: options.label || '', medium_type: options.medium || '', medium_length: options.length || '', + path_data: options.pathData || '', // Save manual path to database token: $('input[name="token"]').val() }, dataType: 'json', @@ -5226,6 +7362,7 @@ dataType: 'json', success: function(response) { if (response.success) { + self.hideConnectionPopup(); self.showMessage('Verbindung gelöscht', 'success'); self.loadConnections(); } @@ -5233,6 +7370,596 @@ }); }, + // Show terminal context menu - choice between Anschlusspunkt (input) and Abgang (output) + showOutputDialog: function(eqId, termId, x, y) { + var self = this; + + // Remove any existing popup + this.hideConnectionPopup(); + this.hideEquipmentPopup(); + $('.schematic-terminal-menu').remove(); + $('.schematic-output-dialog').remove(); + + // Check for existing connections on this terminal + var existingInput = this.connections.find(function(c) { + return !c.fk_source && c.fk_target == eqId && c.target_terminal_id === termId; + }); + var existingOutput = this.connections.find(function(c) { + return c.fk_source == eqId && c.source_terminal_id === termId && !c.fk_target; + }); + + // Show context menu + var html = '
'; + + // Anschlusspunkt (Input) option + html += '
'; + html += ''; + html += 'Anschlusspunkt (L1/L2/L3)'; + if (existingInput) html += ''; + html += '
'; + + // Abgang (Output) option + html += '
'; + html += ''; + html += 'Abgang (Verbraucher/N)'; + if (existingOutput) html += ''; + html += '
'; + + html += '
'; + + $('body').append(html); + + // Hover effect + $('.terminal-menu-item').hover( + function() { $(this).css('background', '#3a3a5a'); }, + function() { + var $item = $(this); + if ($item.hasClass('terminal-menu-input') && existingInput) { + $item.css('background', '#1a4a1a'); + } else if ($item.hasClass('terminal-menu-output') && existingOutput) { + $item.css('background', '#1a4a1a'); + } else { + $item.css('background', ''); + } + } + ); + + // Click handlers + $('.terminal-menu-input').on('click', function() { + $('.schematic-terminal-menu').remove(); + self.showInputDialog(eqId, termId, x, y, existingInput); + }); + + $('.terminal-menu-output').on('click', function() { + $('.schematic-terminal-menu').remove(); + self.showAbgangDialog(eqId, termId, x, y, existingOutput); + }); + + // Close on click outside + setTimeout(function() { + $(document).one('click', function() { + $('.schematic-terminal-menu').remove(); + }); + }, 100); + + // Close on Escape + $(document).one('keydown.terminalMenu', function(e) { + if (e.key === 'Escape') { + $('.schematic-terminal-menu').remove(); + } + }); + }, + + // Show dialog for creating an INPUT connection (Anschlusspunkt for L1, L2, L3) + showInputDialog: function(eqId, termId, x, y, existingInput) { + var self = this; + $('.schematic-output-dialog').remove(); + + var html = '
'; + + html += '

' + + ' Anschlusspunkt (Eingang)

'; + + // Phase selection + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Bezeichnung (optional) + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Buttons + html += '
'; + if (existingInput) { + html += ''; + } + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.input-cancel-btn').on('click', function() { $('.schematic-output-dialog').remove(); }); + $('.input-delete-btn').on('click', function() { + var id = $(this).data('id'); + $('.schematic-output-dialog').remove(); + self.deleteConnection(id); + }); + $('.input-save-btn').on('click', function() { + var phase = $('.input-phase').val(); + var label = $('.input-label').val(); + if (existingInput) { + self.updateInput(existingInput.id, phase, label); + } else { + self.createInput(eqId, termId, phase, label); + } + $('.schematic-output-dialog').remove(); + }); + + $(document).one('keydown.inputDialog', function(e) { + if (e.key === 'Escape') $('.schematic-output-dialog').remove(); + }); + }, + + // Show dialog for creating an OUTPUT connection (Abgang for Verbraucher, N) + showAbgangDialog: function(eqId, termId, x, y, existingOutput) { + var self = this; + $('.schematic-output-dialog').remove(); + + var html = '
'; + + html += '

' + + ' Abgang (Ausgang)

'; + + // Bezeichnung (Label) + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Kabeltyp + html += '
'; + html += ''; + html += '
'; + + // Kabelgröße + html += '
'; + html += ''; + html += '
'; + + // Adernzahl + Phasentyp in einer Zeile + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + + // Buttons + html += '
'; + if (existingOutput) { + html += ''; + } + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + $('.output-label').focus(); + + $('.output-cancel-btn').on('click', function() { $('.schematic-output-dialog').remove(); }); + $('.output-delete-btn').on('click', function() { + var id = $(this).data('id'); + $('.schematic-output-dialog').remove(); + self.deleteConnection(id); + }); + $('.output-save-btn').on('click', function() { + var label = $('.output-label').val(); + var cableType = $('.output-cable-type').val(); + var cableSize = $('.output-cable-size').val(); + var cableCores = $('.output-cable-cores').val(); + var phaseType = $('.output-phase-type').val(); + var mediumSpec = cableCores + 'x' + (cableSize || '1.5'); + + if (existingOutput) { + self.updateOutput(existingOutput.id, label, cableType, mediumSpec, phaseType); + } else { + self.createOutput(eqId, termId, label, cableType, mediumSpec, phaseType); + } + $('.schematic-output-dialog').remove(); + }); + + $(document).one('keydown.outputDialog', function(e) { + if (e.key === 'Escape') $('.schematic-output-dialog').remove(); + }); + + // Adjust position + setTimeout(function() { + var $dialog = $('.schematic-output-dialog'); + var dw = $dialog.outerWidth(), dh = $dialog.outerHeight(); + var ww = $(window).width(), wh = $(window).height(); + if (x + dw > ww - 10) $dialog.css('left', (ww - dw - 10) + 'px'); + if (y + dh > wh - 10) $dialog.css('top', (wh - dh - 10) + 'px'); + }, 10); + }, + + // Create INPUT connection (external source -> equipment terminal) + createInput: function(eqId, termId, phase, label) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'create', + fk_source: '', // NULL = external input + source_terminal_id: '', + fk_target: eqId, + target_terminal_id: termId, + connection_type: phase, + output_label: label, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Anschlusspunkt erstellt', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + response.error, 'error'); + } + } + }); + }, + + // Update INPUT connection + updateInput: function(connId, phase, label) { + var self = this; + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: connId, + connection_type: phase, + output_label: label, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Anschlusspunkt aktualisiert', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + response.error, 'error'); + } + } + }); + }, + + // Create a new cable output (no target, fk_target = NULL) + createOutput: function(eqId, termId, label, cableType, cableSpec, phaseType) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'create', + fk_source: eqId, + source_terminal_id: termId, + fk_target: '', // NULL target = output/endpoint + target_terminal_id: '', + connection_type: phaseType || 'L1N', + output_label: label, + medium_type: cableType, + medium_spec: cableSpec, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Abgang erstellt', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + response.error, 'error'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + + // Update existing output + updateOutput: function(connId, label, cableType, cableSpec, phaseType) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: connId, + connection_type: phaseType || 'L1N', + output_label: label, + medium_type: cableType, + medium_spec: cableSpec, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Abgang aktualisiert', 'success'); + self.loadConnections(); + } else { + self.showMessage('Fehler: ' + response.error, 'error'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + + showConnectionPopup: function(connId, x, y) { + var self = this; + console.log('showConnectionPopup called with connId:', connId, 'type:', typeof connId); + console.log('Available connections:', this.connections.map(function(c) { return c.id; })); + + // Remove any existing popup + this.hideConnectionPopup(); + + // Find connection data (convert to int for comparison) + var connIdInt = parseInt(connId); + var conn = this.connections.find(function(c) { return parseInt(c.id) === connIdInt; }); + console.log('Found connection:', conn); + if (!conn) { + console.warn('Connection not found for id:', connId); + return; + } + + // Create popup - use very high z-index to ensure visibility + console.log('Creating popup at position:', x, y); + var popupHtml = '
'; + + // Edit button + popupHtml += ''; + + // Delete button + popupHtml += ''; + + popupHtml += '
'; + + $('body').append(popupHtml); + console.log('Popup appended to body, checking if exists:', $('.schematic-connection-popup').length); + + // Bind button events + $('.schematic-popup-edit').on('click', function(e) { + e.stopPropagation(); + self.editConnection(connId); + }); + + $('.schematic-popup-delete').on('click', function(e) { + e.stopPropagation(); + self.hideConnectionPopup(); + self.deleteConnection(connId); + }); + + // Adjust position if popup goes off screen + var $popup = $('.schematic-connection-popup'); + var popupWidth = $popup.outerWidth(); + var popupHeight = $popup.outerHeight(); + var windowWidth = $(window).width(); + var windowHeight = $(window).height(); + + if (x + popupWidth > windowWidth - 10) { + $popup.css('left', (x - popupWidth) + 'px'); + } + if (y + popupHeight > windowHeight - 10) { + $popup.css('top', (y - popupHeight) + 'px'); + } + }, + + hideConnectionPopup: function() { + $('.schematic-connection-popup').remove(); + }, + + editConnection: function(connId) { + var self = this; + + // Hide popup first + this.hideConnectionPopup(); + + // Find connection data + var conn = this.connections.find(function(c) { return c.id == connId; }); + if (!conn) return; + + // Build edit dialog + var dialogHtml = '
'; + + dialogHtml += '

Verbindung bearbeiten

'; + + // Connection type + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Output label + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Color + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Medium type + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Medium spec + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Medium length + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Buttons + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += ''; + dialogHtml += '
'; + + dialogHtml += '
'; + + // Overlay + var overlayHtml = '
'; + + $('body').append(overlayHtml).append(dialogHtml); + + // Bind events + $('.edit-dialog-cancel, .schematic-edit-overlay').on('click', function() { + self.closeEditDialog(); + }); + + $('.edit-dialog-save').on('click', function() { + self.saveConnectionEdit(connId); + }); + + // Enter key to save + $('.schematic-edit-dialog input').on('keypress', function(e) { + if (e.which === 13) { + self.saveConnectionEdit(connId); + } + }); + }, + + closeEditDialog: function() { + $('.schematic-edit-dialog, .schematic-edit-overlay').remove(); + }, + + saveConnectionEdit: function(connId) { + var self = this; + + var data = { + action: 'update', + connection_id: connId, + connection_type: $('.edit-connection-type').val(), + output_label: $('.edit-output-label').val(), + color: $('.edit-connection-color').val(), + medium_type: $('.edit-medium-type').val(), + medium_spec: $('.edit-medium-spec').val(), + medium_length: $('.edit-medium-length').val(), + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + self.closeEditDialog(); + self.showMessage('Verbindung aktualisiert', 'success'); + self.loadConnections(); + } else { + self.showMessage(response.error || 'Fehler beim Speichern', 'error'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + clearAllConnections: function() { var self = this; @@ -5255,26 +7982,270 @@ }); }, + // Equipment Popup functions + showEquipmentPopup: function(equipmentId, x, y) { + var self = this; + + // Remove any existing popup + this.hideEquipmentPopup(); + this.hideConnectionPopup(); + + // Find equipment data + var eq = this.equipment.find(function(e) { return parseInt(e.id) === parseInt(equipmentId); }); + if (!eq) { + console.warn('Equipment not found for id:', equipmentId); + return; + } + + // Create popup + var popupHtml = '
'; + + // Equipment info + popupHtml += '
'; + popupHtml += '' + self.escapeHtml(eq.type_label || eq.label || 'Equipment') + ''; + if (eq.label && eq.type_label) { + popupHtml += '
' + self.escapeHtml(eq.label) + ''; + } + popupHtml += '
'; + + // Buttons row + popupHtml += '
'; + + // Edit button + popupHtml += ''; + + // Delete button + popupHtml += ''; + + popupHtml += '
'; + + $('body').append(popupHtml); + + // Bind button events + $('.schematic-eq-popup-edit').on('click', function(e) { + e.stopPropagation(); + self.editEquipment(equipmentId); + }); + + $('.schematic-eq-popup-delete').on('click', function(e) { + e.stopPropagation(); + self.hideEquipmentPopup(); + self.deleteEquipment(equipmentId); + }); + + // Adjust position if popup goes off screen + var $popup = $('.schematic-equipment-popup'); + var popupWidth = $popup.outerWidth(); + var popupHeight = $popup.outerHeight(); + var windowWidth = $(window).width(); + var windowHeight = $(window).height(); + + if (x + popupWidth > windowWidth - 10) { + $popup.css('left', (x - popupWidth) + 'px'); + } + if (y + popupHeight > windowHeight - 10) { + $popup.css('top', (y - popupHeight) + 'px'); + } + }, + + hideEquipmentPopup: function() { + $('.schematic-equipment-popup').remove(); + }, + + editEquipment: function(equipmentId) { + var self = this; + + // Hide popup first + this.hideEquipmentPopup(); + + // Find equipment data + var eq = this.equipment.find(function(e) { return parseInt(e.id) === parseInt(equipmentId); }); + if (!eq) return; + + // Load equipment types for dropdown + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'get_types', system_id: 1 }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showEditEquipmentDialog(eq, response.types); + } + } + }); + }, + + showEditEquipmentDialog: function(eq, types) { + var self = this; + + var dialogHtml = '
'; + + dialogHtml += '
'; + + dialogHtml += '

Equipment bearbeiten

'; + + // Equipment type + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Label + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Position + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += '
'; + + // Buttons + dialogHtml += '
'; + dialogHtml += ''; + dialogHtml += ''; + dialogHtml += '
'; + + dialogHtml += '
'; + + $('body').append(dialogHtml); + + // Bind events + $('.edit-dialog-cancel, .schematic-edit-overlay').on('click', function() { + $('.schematic-edit-dialog, .schematic-edit-overlay').remove(); + }); + + $('.edit-dialog-save').on('click', function() { + self.saveEquipmentEdit(eq.id); + }); + + // Enter key to save + $('.schematic-edit-dialog input').on('keypress', function(e) { + if (e.which === 13) { + self.saveEquipmentEdit(eq.id); + } + }); + }, + + saveEquipmentEdit: function(equipmentId) { + var self = this; + + var data = { + action: 'update', + equipment_id: equipmentId, + type_id: $('.edit-equipment-type').val(), + label: $('.edit-equipment-label').val(), + position_te: $('.edit-equipment-position').val(), + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + $('.schematic-edit-dialog, .schematic-edit-overlay').remove(); + self.showMessage('Equipment aktualisiert', 'success'); + self.loadData(); + } else { + self.showMessage(response.error || 'Fehler beim Speichern', 'error'); + } + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + } + }); + }, + + deleteEquipment: function(equipmentId) { + var self = this; + + this.hideEquipmentPopup(); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'delete', + equipment_id: equipmentId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Equipment gelöscht', 'success'); + // Remove from local array instead of reloading everything + self.equipment = self.equipment.filter(function(e) { + return String(e.id) !== String(equipmentId); + }); + // Also remove connections involving this equipment + self.connections = self.connections.filter(function(c) { + return String(c.fk_source) !== String(equipmentId) && + String(c.fk_target) !== String(equipmentId); + }); + self.render(); + } else { + self.showMessage(response.error || 'Fehler', 'error'); + } + } + }); + }, + // Block dragging startDragBlock: function($block, e) { var eqId = $block.data('equipment-id'); var carrierId = $block.data('carrier-id'); - var eq = this.equipment.find(function(e) { return e.id == eqId; }); - var carrier = this.carriers.find(function(c) { return c.id == carrierId; }); + var eq = this.equipment.find(function(e) { return String(e.id) === String(eqId); }); + var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); }); - if (!eq || !carrier) return; + if (!eq || !carrier) { + console.log('startDragBlock: Equipment or carrier not found', eqId, carrierId); + return; + } + + if (typeof carrier._x === 'undefined') { + console.log('startDragBlock: Carrier has no _x position', carrier); + return; + } var $svg = $(this.svgElement); var offset = $svg.offset(); this.dragState = { type: 'block', + equipmentId: eqId, equipment: eq, carrier: carrier, element: $block, startX: e.pageX, startY: e.pageY, - originalTE: eq.position_te + originalTE: parseInt(eq.position_te) || 1, + originalX: parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2, + originalY: eq._y }; $block.addClass('dragging'); @@ -5287,15 +8258,17 @@ var newTE = this.dragState.originalTE + Math.round(dx / this.TE_WIDTH); // Clamp to valid range - var maxTE = (this.dragState.carrier.total_te || 12) - (this.dragState.equipment.width_te || 1) + 1; + var maxTE = (parseInt(this.dragState.carrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1; newTE = Math.max(1, Math.min(newTE, maxTE)); - // Preview position - var newX = this.dragState.carrier._x + (newTE - 1) * this.TE_WIDTH + 2; - var currentTransform = this.dragState.element.attr('transform'); - var currentY = this.dragState.equipment._y; + // Preview position - use stored originalX and calculate offset from originalTE + var teOffset = newTE - this.dragState.originalTE; + var newX = this.dragState.originalX + teOffset * this.TE_WIDTH; + var currentY = this.dragState.originalY; - this.dragState.element.attr('transform', 'translate(' + newX + ',' + currentY + ')'); + if (!isNaN(newX) && !isNaN(currentY)) { + this.dragState.element.attr('transform', 'translate(' + newX + ',' + currentY + ')'); + } this.dragState.newTE = newTE; }, @@ -5303,12 +8276,16 @@ if (!this.dragState || this.dragState.type !== 'block') return; var newTE = this.dragState.newTE || this.dragState.originalTE; + var element = this.dragState.element; if (newTE !== this.dragState.originalTE) { this.updateEquipmentPosition(this.dragState.equipment.id, newTE); + } else { + // Position didn't change, reset visual position immediately + element.attr('transform', 'translate(' + this.dragState.originalX + ',' + this.dragState.originalY + ')'); } - this.dragState.element.removeClass('dragging'); + element.removeClass('dragging'); this.dragState = null; }, @@ -5327,31 +8304,106 @@ dataType: 'json', success: function(response) { if (response.success) { - // Update local data - var eq = self.equipment.find(function(e) { return e.id == eqId; }); - if (eq) eq.position_te = newTE; + // Update local data and re-render + var eq = self.equipment.find(function(e) { return String(e.id) === String(eqId); }); + if (eq) { + eq.position_te = newTE; + } + // Full re-render to ensure carrier positions are set + self.render(); + } else { + // Revert visual position on error + self.showMessage(response.error || 'Position nicht verfügbar', 'error'); self.render(); } + }, + error: function() { + // Revert on error + self.showMessage('Netzwerkfehler', 'error'); + self.render(); } }); }, + // Zoom functions + setZoom: function(newScale) { + // Clamp zoom level between 0.25 and 2.0 + this.scale = Math.max(0.25, Math.min(2.0, newScale)); + + // Apply transform to wrapper (SVG + controls) + var $wrapper = $('.schematic-zoom-wrapper'); + if ($wrapper.length) { + $wrapper.css('transform', 'scale(' + this.scale + ')'); + $wrapper.css('transform-origin', '0 0'); + + // Update container size to account for scaling + var $svg = $wrapper.find('.schematic-svg'); + if ($svg.length) { + var actualWidth = $svg.attr('width') * this.scale; + var actualHeight = $svg.attr('height') * this.scale; + $wrapper.parent().css({ + 'min-width': actualWidth + 'px', + 'min-height': actualHeight + 'px' + }); + } + } + + // Update zoom display + this.updateZoomDisplay(); + }, + + zoomToFit: function() { + if (!this.svgElement) return; + + var $canvas = $('.schematic-editor-canvas'); + var $svg = $(this.svgElement); + + var canvasWidth = $canvas.width() - 40; // Padding + var canvasHeight = $canvas.height() - 40; + var svgWidth = parseFloat($svg.attr('width')) || 800; + var svgHeight = parseFloat($svg.attr('height')) || 600; + + // Calculate scale to fit both dimensions + var scaleX = canvasWidth / svgWidth; + var scaleY = canvasHeight / svgHeight; + var newScale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 100% + + this.setZoom(newScale); + }, + + updateZoomDisplay: function() { + var percentage = Math.round(this.scale * 100); + $('.schematic-zoom-level').text(percentage + '%'); + }, + showMessage: function(text, type) { var $msg = $('.schematic-message'); if (!$msg.length) { - $msg = $('
'); + // Create permanent status bar with fixed height + $msg = $('
'); $('.schematic-editor-canvas').prepend($msg); } - $msg.removeClass('success error warning info').addClass(type).text(text).fadeIn(); + // Clear any pending hide timeout + if (this.messageTimeout) { + clearTimeout(this.messageTimeout); + this.messageTimeout = null; + } + + $msg.removeClass('success error warning info').addClass(type).text(text).css('opacity', 1); if (type === 'success' || type === 'error') { - setTimeout(function() { $msg.fadeOut(); }, 2000); + this.messageTimeout = setTimeout(function() { + // Don't hide, just clear the message text but keep the bar + $msg.removeClass('success error warning info').addClass('info').text('Bereit'); + }, 2500); } }, hideMessage: function() { - $('.schematic-message').fadeOut(); + var $msg = $('.schematic-message'); + // Don't hide, just reset to default state + $msg.removeClass('success error warning info').addClass('info').text('Bereit'); }, escapeHtml: function(text) { diff --git a/js/pathfinding.min.js b/js/pathfinding.min.js new file mode 100644 index 0000000..76ec784 --- /dev/null +++ b/js/pathfinding.min.js @@ -0,0 +1 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.PF=t()}}(function(){return function t(e,r,i){function n(s,a){if(!r[s]){if(!e[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(o)return o(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var l=r[s]={exports:{}};e[s][0].call(l.exports,function(t){var r=e[s][1][t];return n(r?r:t)},l,l.exports,t,e,r,i)}return r[s].exports}for(var o="function"==typeof require&&require,s=0;st?-1:t>e?1:0},h=function(t,e,n,o,s){var a;if(null==n&&(n=0),null==s&&(s=r),0>n)throw new Error("lo must be non-negative");for(null==o&&(o=t.length);o>n;)a=i((n+o)/2),s(e,t[a])<0?o=a:n=a+1;return[].splice.apply(t,[n,n-n].concat(e)),e},s=function(t,e,i){return null==i&&(i=r),t.push(e),d(t,0,t.length-1,i)},o=function(t,e){var i,n;return null==e&&(e=r),i=t.pop(),t.length?(n=t[0],t[0]=i,g(t,0,e)):n=i,n},u=function(t,e,i){var n;return null==i&&(i=r),n=t[0],t[0]=e,g(t,0,i),n},a=function(t,e,i){var n;return null==i&&(i=r),t.length&&i(t[0],e)<0&&(n=[t[0],e],e=n[0],t[0]=n[1],g(t,0,i)),e},n=function(t,e){var n,o,s,a,u,h;for(null==e&&(e=r),a=function(){h=[];for(var e=0,r=i(t.length/2);r>=0?r>e:e>r;r>=0?e++:e--)h.push(e);return h}.apply(this).reverse(),u=[],o=0,s=a.length;s>o;o++)n=a[o],u.push(g(t,n,e));return u},f=function(t,e,i){var n;return null==i&&(i=r),n=t.indexOf(e),-1!==n?(d(t,0,n,i),g(t,n,i)):void 0},p=function(t,e,i){var o,s,u,h,l;if(null==i&&(i=r),s=t.slice(0,e),!s.length)return s;for(n(s,i),l=t.slice(e),u=0,h=l.length;h>u;u++)o=l[u],a(s,o,i);return s.sort(i).reverse()},c=function(t,e,i){var s,a,u,p,c,f,d,g,b,y;if(null==i&&(i=r),10*e<=t.length){if(p=t.slice(0,e).sort(i),!p.length)return p;for(u=p[p.length-1],g=t.slice(e),c=0,d=g.length;d>c;c++)s=g[c],i(s,u)<0&&(h(p,s,0,null,i),p.pop(),u=p[p.length-1]);return p}for(n(t,i),y=[],a=f=0,b=l(e,t.length);b>=0?b>f:f>b;a=b>=0?++f:--f)y.push(o(t,i));return y},d=function(t,e,i,n){var o,s,a;for(null==n&&(n=r),o=t[i];i>e&&(a=i-1>>1,s=t[a],n(o,s)<0);)t[i]=s,i=a;return t[i]=o},g=function(t,e,i){var n,o,s,a,u;for(null==i&&(i=r),o=t.length,u=e,s=t[e],n=2*e+1;o>n;)a=n+1,o>a&&!(i(t[n],t[a])<0)&&(n=a),t[e]=t[n],e=n,n=2*e+1;return t[e]=s,d(t,u,e,i)},t=function(){function t(t){this.cmp=null!=t?t:r,this.nodes=[]}return t.push=s,t.pop=o,t.replace=u,t.pushpop=a,t.heapify=n,t.nlargest=p,t.nsmallest=c,t.prototype.push=function(t){return s(this.nodes,t,this.cmp)},t.prototype.pop=function(){return o(this.nodes,this.cmp)},t.prototype.peek=function(){return this.nodes[0]},t.prototype.contains=function(t){return-1!==this.nodes.indexOf(t)},t.prototype.replace=function(t){return u(this.nodes,t,this.cmp)},t.prototype.pushpop=function(t){return a(this.nodes,t,this.cmp)},t.prototype.heapify=function(){return n(this.nodes,this.cmp)},t.prototype.updateItem=function(t){return f(this.nodes,t,this.cmp)},t.prototype.clear=function(){return this.nodes=[]},t.prototype.empty=function(){return 0===this.nodes.length},t.prototype.size=function(){return this.nodes.length},t.prototype.clone=function(){var e;return e=new t,e.nodes=this.nodes.slice(0),e},t.prototype.toArray=function(){return this.nodes.slice(0)},t.prototype.insert=t.prototype.push,t.prototype.remove=t.prototype.pop,t.prototype.top=t.prototype.peek,t.prototype.front=t.prototype.peek,t.prototype.has=t.prototype.contains,t.prototype.copy=t.prototype.clone,t}(),("undefined"!=typeof e&&null!==e?e.exports:void 0)?e.exports=t:window.Heap=t}.call(this)},{}],3:[function(t,e){function r(t,e,r){this.width=t,this.height=e,this.nodes=this._buildNodes(t,e,r)}var i=t("./Node");r.prototype._buildNodes=function(t,e,r){var n,o,s=new Array(e);for(n=0;e>n;++n)for(s[n]=new Array(t),o=0;t>o;++o)s[n][o]=new i(o,n);if(void 0===r)return s;if(r.length!==e||r[0].length!==t)throw new Error("Matrix size does not fit");for(n=0;e>n;++n)for(o=0;t>o;++o)r[n][o]&&(s[n][o].walkable=!1);return s},r.prototype.getNodeAt=function(t,e){return this.nodes[e][t]},r.prototype.isWalkableAt=function(t,e){return this.isInside(t,e)&&this.nodes[e][t].walkable},r.prototype.isInside=function(t,e){return t>=0&&t=0&&et;++t)for(u[t]=new Array(n),e=0;n>e;++e)u[t][e]=new i(e,t,s[t][e].walkable);return a.nodes=u,a},e.exports=r},{"./Node":5}],4:[function(t,e){e.exports={manhattan:function(t,e){return t+e},euclidean:function(t,e){return Math.sqrt(t*t+e*e)},octile:function(t,e){var r=Math.SQRT2-1;return e>t?r*t+e:r*e+t},chebyshev:function(t,e){return Math.max(t,e)}}},{}],5:[function(t,e){function r(t,e,r){this.x=t,this.y=e,this.walkable=void 0===r?!0:r}e.exports=r},{}],6:[function(t,e,r){function i(t){for(var e=[[t.x,t.y]];t.parent;)t=t.parent,e.push([t.x,t.y]);return e.reverse()}function n(t,e){var r=i(t),n=i(e);return r.concat(n.reverse())}function o(t){var e,r,i,n,o,s=0;for(e=1;et?1:-1,o=i>e?1:-1,u=s-a;;){if(p.push([t,e]),t===r&&e===i)break;h=2*u,h>-a&&(u-=a,t+=n),s>h&&(u+=s,e+=o)}return p}function a(t){var e,r,i,n,o,a,u=[],h=t.length;if(2>h)return u;for(o=0;h-1>o;++o)for(e=t[o],r=t[o+1],i=s(e[0],e[1],r[0],r[1]),n=i.length,a=0;n-1>a;++a)u.push(i[a]);return u.push(t[h-1]),u}function u(t,e){var r,i,n,o,a,u,h,l,p,c,f,d=e.length,g=e[0][0],b=e[0][1],y=e[d-1][0],A=e[d-1][1];for(r=g,i=b,a=[[r,i]],u=2;d>u;++u){for(l=e[u],n=l[0],o=l[1],p=s(r,i,n,o),f=!1,h=1;hl;++l)h=u[l],h.closed||(c=h.x,f=h.y,d=a.g+(0===c-a.x||0===f-a.y?1:x),(!h.opened||dl;++l)if(h=u[l],!h.closed){if(h.opened===C)return n.biBacktrace(a,h);c=h.x,f=h.y,d=a.g+(0===c-a.x||0===f-a.y?1:W),(!h.opened||dl;++l)if(h=u[l],!h.closed){if(h.opened===N)return n.biBacktrace(h,a);c=h.x,f=h.y,d=a.g+(0===c-a.x||0===f-a.y?1:W),(!h.opened||dh;++h)if(a=s[h],!a.closed)if(a.opened){if(a.by===A)return i.biBacktrace(u,a)}else f.push(a),a.parent=u,a.opened=!0,a.by=y;for(u=d.shift(),u.closed=!0,s=o.getNeighbors(u,g,b),h=0,l=s.length;l>h;++h)if(a=s[h],!a.closed)if(a.opened){if(a.by===y)return i.biBacktrace(a,u)}else d.push(a),a.parent=u,a.opened=!0,a.by=A}return[]},e.exports=r},{"../core/Util":6}],12:[function(t,e){function r(t){i.call(this,t),this.heuristic=function(){return 0}}var i=t("./BiAStarFinder");r.prototype=new i,r.prototype.constructor=r,e.exports=r},{"./BiAStarFinder":9}],13:[function(t,e){function r(t){t=t||{},this.allowDiagonal=t.allowDiagonal,this.dontCrossCorners=t.dontCrossCorners}var i=t("../core/Util");r.prototype.findPath=function(t,e,r,n,o){var s,a,u,h,l,p=[],c=this.allowDiagonal,f=this.dontCrossCorners,d=o.getNodeAt(t,e),g=o.getNodeAt(r,n);for(p.push(d),d.opened=!0;p.length;){if(u=p.shift(),u.closed=!0,u===g)return i.backtrace(g);for(s=o.getNeighbors(u,c,f),h=0,l=s.length;l>h;++h)a=s[h],a.closed||a.opened||(p.push(a),a.opened=!0,a.parent=u)}return[]},e.exports=r},{"../core/Util":6}],14:[function(t,e){function r(t){i.call(this,t),this.heuristic=function(){return 0}}var i=t("./AStarFinder");r.prototype=new i,r.prototype.constructor=r,e.exports=r},{"./AStarFinder":7}],15:[function(t,e){function r(t){t=t||{},this.allowDiagonal=t.allowDiagonal,this.dontCrossCorners=t.dontCrossCorners,this.heuristic=t.heuristic||i.manhattan,this.weight=t.weight||1,this.trackRecursion=t.trackRecursion||!1,this.timeLimit=t.timeLimit||1/0}t("../core/Util");var i=t("../core/Heuristic"),n=t("../core/Node");r.prototype.findPath=function(t,e,r,i,o){var s,a,u,h=0,l=(new Date).getTime(),p=function(t,e){return this.heuristic(Math.abs(e.x-t.x),Math.abs(e.y-t.y))}.bind(this),c=function(t,e){return t.x===e.x||t.y===e.y?1:Math.SQRT2},f=function(t,e,r,i,s){if(h++,this.timeLimit>0&&(new Date).getTime()-l>1e3*this.timeLimit)return 1/0;var a=e+p(t,g)*this.weight;if(a>r)return a;if(t==g)return i[s]=[t.x,t.y],t;var u,d,b,y,A=o.getNeighbors(t,this.allowDiagonal,this.dontCrossCorners);for(b=0,u=1/0;y=A[b];++b){if(this.trackRecursion&&(y.retainCount=y.retainCount+1||1,y.tested!==!0&&(y.tested=!0)),d=f(y,e+c(t,y),r,i,s+1),d instanceof n)return i[s]=[t.x,t.y],d;this.trackRecursion&&0===--y.retainCount&&(y.tested=!1),u>d&&(u=d)}return u}.bind(this),d=o.getNodeAt(t,e),g=o.getNodeAt(r,i),b=p(d,g);for(s=0;!0;++s){if(a=[],u=f(d,0,b,a,0),1/0===u)return[];if(u instanceof n)return a;b=u}return[]},e.exports=r},{"../core/Heuristic":4,"../core/Node":5,"../core/Util":6}],16:[function(t,e){function r(t){t=t||{},this.heuristic=t.heuristic||o.manhattan,this.trackJumpRecursion=t.trackJumpRecursion||!1}var i=t("heap"),n=t("../core/Util"),o=t("../core/Heuristic");r.prototype.findPath=function(t,e,r,o,s){var a,u=this.openList=new i(function(t,e){return t.f-e.f}),h=this.startNode=s.getNodeAt(t,e),l=this.endNode=s.getNodeAt(r,o);for(this.grid=s,h.g=0,h.f=0,u.push(h),h.opened=!0;!u.empty();){if(a=u.pop(),a.closed=!0,a===l)return n.expandPath(n.backtrace(l));this._identifySuccessors(a)}return[]},r.prototype._identifySuccessors=function(t){var e,r,i,n,s,a,u,h,l,p,c=this.grid,f=this.heuristic,d=this.openList,g=this.endNode.x,b=this.endNode.y,y=t.x,A=t.y,k=Math.abs;for(Math.max,e=this._findNeighbors(t),n=0,s=e.length;s>n;++n)if(r=e[n],i=this._jump(r[0],r[1],y,A)){if(a=i[0],u=i[1],p=c.getNodeAt(a,u),p.closed)continue;h=o.octile(k(a-y),k(u-A)),l=t.g+h,(!p.opened||la;++a)s=o[a],f.push([s.x,s.y]);return f},e.exports=r},{"../core/Heuristic":4,"../core/Util":6,heap:1}],17:[function(t,e){function r(t){n.call(this,t),t=t||{},this.heuristic=t.heuristic||i.manhattan}var i=t("../core/Heuristic"),n=t("./JumpPointFinder");r.prototype=new n,r.prototype.constructor=r,r.prototype._jump=function(t,e,r,i){var n=this.grid,o=t-r,s=e-i;if(!n.isWalkableAt(t,e))return null;if(this.trackJumpRecursion===!0&&(n.getNodeAt(t,e).tested=!0),n.getNodeAt(t,e)===this.endNode)return[t,e];if(0!==o){if(n.isWalkableAt(t,e-1)&&!n.isWalkableAt(t-o,e-1)||n.isWalkableAt(t,e+1)&&!n.isWalkableAt(t-o,e+1))return[t,e]}else{if(0===s)throw new Error("Only horizontal and vertical movements are allowed");if(n.isWalkableAt(t-1,e)&&!n.isWalkableAt(t-1,e-s)||n.isWalkableAt(t+1,e)&&!n.isWalkableAt(t+1,e-s))return[t,e];if(this._jump(t+1,e,t,e)||this._jump(t-1,e,t,e))return[t,e]}return this._jump(t+o,e+s,t,e)},r.prototype._findNeighbors=function(t){var e,r,i,n,o,s,a,u,h=t.parent,l=t.x,p=t.y,c=this.grid,f=[];if(h)e=h.x,r=h.y,i=(l-e)/Math.max(Math.abs(l-e),1),n=(p-r)/Math.max(Math.abs(p-r),1),0!==i?(c.isWalkableAt(l,p-1)&&f.push([l,p-1]),c.isWalkableAt(l,p+1)&&f.push([l,p+1]),c.isWalkableAt(l+i,p)&&f.push([l+i,p])):0!==n&&(c.isWalkableAt(l-1,p)&&f.push([l-1,p]),c.isWalkableAt(l+1,p)&&f.push([l+1,p]),c.isWalkableAt(l,p+n)&&f.push([l,p+n]));else for(o=c.getNeighbors(t,!1),a=0,u=o.length;u>a;++a)s=o[a],f.push([s.x,s.y]);return f},e.exports=r},{"../core/Heuristic":4,"./JumpPointFinder":16}],18:[function(t,e){e.exports={Heap:t("heap"),Node:t("./core/Node"),Grid:t("./core/Grid"),Util:t("./core/Util"),Heuristic:t("./core/Heuristic"),AStarFinder:t("./finders/AStarFinder"),BestFirstFinder:t("./finders/BestFirstFinder"),BreadthFirstFinder:t("./finders/BreadthFirstFinder"),DijkstraFinder:t("./finders/DijkstraFinder"),BiAStarFinder:t("./finders/BiAStarFinder"),BiBestFirstFinder:t("./finders/BiBestFirstFinder"),BiBreadthFirstFinder:t("./finders/BiBreadthFirstFinder"),BiDijkstraFinder:t("./finders/BiDijkstraFinder"),IDAStarFinder:t("./finders/IDAStarFinder"),JumpPointFinder:t("./finders/JumpPointFinder"),OrthogonalJumpPointFinder:t("./finders/OrthogonalJumpPointFinder")}},{"./core/Grid":3,"./core/Heuristic":4,"./core/Node":5,"./core/Util":6,"./finders/AStarFinder":7,"./finders/BestFirstFinder":8,"./finders/BiAStarFinder":9,"./finders/BiBestFirstFinder":10,"./finders/BiBreadthFirstFinder":11,"./finders/BiDijkstraFinder":12,"./finders/BreadthFirstFinder":13,"./finders/DijkstraFinder":14,"./finders/IDAStarFinder":15,"./finders/JumpPointFinder":16,"./finders/OrthogonalJumpPointFinder":17,heap:1}]},{},[18])(18)}); \ No newline at end of file diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql index 5026bb4..4c99eda 100755 --- a/sql/dolibarr_allversions.sql +++ b/sql/dolibarr_allversions.sql @@ -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; diff --git a/sql/llx_kundenkarte_equipment_connection.sql b/sql/llx_kundenkarte_equipment_connection.sql index bf248cc..eb71566 100644 --- a/sql/llx_kundenkarte_equipment_connection.sql +++ b/sql/llx_kundenkarte_equipment_connection.sql @@ -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, diff --git a/sql/llx_kundenkarte_equipment_type.sql b/sql/llx_kundenkarte_equipment_type.sql index c731b37..98139cd 100644 --- a/sql/llx_kundenkarte_equipment_type.sql +++ b/sql/llx_kundenkarte_equipment_type.sql @@ -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, diff --git a/tabs/anlagen.php b/tabs/anlagen.php index db4710b..a91073c 100755 --- a/tabs/anlagen.php +++ b/tabs/anlagen.php @@ -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 '

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

'; + print '

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

'; - // Equipment container + // Equipment container - nur noch der SchematicEditor print '
'; - // 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 '
'; - print '

'.$langs->trans('Panels').' ('.$langs->trans('Fields').')

'; - if ($permissiontoadd) { - print ''; - print ' '.$langs->trans('AddPanel'); - print ''; - } - print '
'; - - // Panels container (horizontal scrolling) - print '
'; - - 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 '
'; - print ''; - print '
'; - } - } - - // Direct carriers (without panel) - legacy support - if (!empty($directCarriers)) { - print '
'; - print '
'; - print ''.$langs->trans('DirectCarriers').''; - if ($permissiontoadd) { - print ''; - print ''; - print ''; - } - print '
'; - print '
'; - foreach ($directCarriers as $c) { - $c->fetchEquipment(); - print renderCarrierHTML($c, $langs, $permissiontoadd, $permissiontodelete); - } - print '
'; - print '
'; - } - - // Show "Add Panel" placeholder if no panels and no direct carriers - if (empty($panels) && empty($directCarriers)) { - print '
'; - print $langs->trans('NoPanelsOrCarriers').' - '; - print $langs->trans('AddPanelOrCarrier'); - print '
'; - } - - print '
'; // .kundenkarte-panels-container - - // Schematic Editor - Interactive Connection Editor (always expanded) - print '
'; + // Schematic Editor - Hauptansicht + print '
'; print '
'; print '
'; - print ''.$langs->trans('ConnectionEditor').' (Klick auf Terminal → Klick auf Ziel-Terminal = Verbindung | Rechtsklick = Löschen)'; + print ''.$langs->trans('SchematicEditor').' (Klick auf Block = Bearbeiten | Drag = Verschieben | + = Hinzufügen)'; print '
'; - print '
'; + print '
'; + // Zoom controls + print '
'; + print ''; + print '100%'; + print ''; + print ''; + print ''; + print '
'; + // Manual wire draw toggle + print ''; + print ''; print ''; + // PDF Export button + $pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L'; + print ''; + print ' PDF Export'; + print ''; print '
'; print '
'; print '
'; @@ -642,402 +592,14 @@ if (empty($customerSystems)) { print '
'; // .kundenkarte-equipment-container - // Initialize Equipment JavaScript + // Initialize SchematicEditor JavaScript print ''; - - // === Interactive Connection Editor (Prototype) === - // Pure SVG solution - no external libraries needed - - print '
'; - print '
'; - print ' Interaktiver Verbindungseditor (Prototyp) '; - print ''; - print '
'; - - print ''; // #jsplumb-container - print '
'; // .kundenkarte-jsplumb-prototype } // Action buttons