/** * KundenKarte Module JavaScript * Copyright (C) 2026 Alles Watt lauft */ (function() { 'use strict'; // 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(); } }); }; // Dialog zum Ausbauen mit Datumsauswahl KundenKarte.showDecommissionDialog = function(anlageId, onSuccess) { $('#kundenkarte-decommission-dialog').remove(); var today = new Date().toISOString().split('T')[0]; var html = '
'; html += '
'; html += '

Element ausbauen

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

Element als ausgebaut markieren?

'; html += ''; html += ''; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-decommission-dialog').addClass('visible'); $('#decommission-date').focus(); $('#decommission-confirm').on('click', function() { var dateVal = $('#decommission-date').val(); $('#kundenkarte-decommission-dialog').remove(); $(document).off('keydown.decommDialog'); $.post(baseUrl + '/custom/kundenkarte/ajax/anlage.php', { action: 'toggle_decommissioned', anlage_id: anlageId, date_decommissioned: dateVal, token: $('input[name="token"]').val() || '' }, function(res) { if (res.success) { if (typeof onSuccess === 'function') { onSuccess(res); } else { location.reload(); } } else { alert(res.error || 'Fehler'); } }, 'json'); }); $('#decommission-cancel, #kundenkarte-decommission-dialog .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-decommission-dialog').remove(); $(document).off('keydown.decommDialog'); }); $(document).on('keydown.decommDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-decommission-dialog').remove(); $(document).off('keydown.decommDialog'); } else if (e.key === 'Enter') { $('#decommission-confirm').click(); } }); }; // Global error display with details KundenKarte.showError = function(title, message, details) { $('#kundenkarte-error-dialog').remove(); var html = '
'; html += '
'; html += '

' + escapeHtml(title || 'Fehler') + '

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

' + escapeHtml(message || 'Ein unbekannter Fehler ist aufgetreten.') + '

'; if (details) { html += '
Technische Details'; html += '
' + escapeHtml(details) + '
'; html += '
'; } html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-error-dialog').addClass('visible'); $('#error-ok').focus(); var closeDialog = function() { $('#kundenkarte-error-dialog').remove(); $(document).off('keydown.errorDialog'); }; $('#error-ok, #kundenkarte-error-dialog .kundenkarte-modal-close').on('click', closeDialog); $(document).on('keydown.errorDialog', function(e) { if (e.key === 'Escape' || e.key === 'Enter') closeDialog(); }); }; // Global success/info notification (non-blocking) KundenKarte.showNotification = function(message, type) { type = type || 'success'; var bgColor = type === 'success' ? '#27ae60' : (type === 'warning' ? '#f39c12' : (type === 'error' ? '#e74c3c' : '#3498db')); var icon = type === 'success' ? 'fa-check' : (type === 'warning' ? 'fa-exclamation' : (type === 'error' ? 'fa-times' : 'fa-info')); var $note = $('
' + escapeHtml(message) + '
'); $('body').append($note); setTimeout(function() { $note.fadeOut(300, function() { $(this).remove(); }); }, 3000); }; // Get base URL for AJAX calls var baseUrl = (typeof DOL_URL_ROOT !== 'undefined') ? DOL_URL_ROOT : ''; if (!baseUrl) { // Try to detect from script src var scripts = document.getElementsByTagName('script'); for (var i = 0; i < scripts.length; i++) { var src = scripts[i].src; if (src && src.indexOf('/kundenkarte/js/kundenkarte.js') > -1) { baseUrl = src.replace('/custom/kundenkarte/js/kundenkarte.js', '').replace(/\?.*$/, ''); break; } } } /** * Tree Component */ KundenKarte.Tree = { tooltipTimeout: null, hideTimeout: null, currentTooltip: null, currentItem: null, draggedNode: null, isDragging: false, dropTarget: null, init: function() { this.bindEvents(); this.initDragDrop(); this.initCompactMode(); }, bindEvents: function() { var self = this; // Toggle tree nodes - MUST use stopImmediatePropagation for delegated handlers on same element $(document).on('click', '.kundenkarte-tree-toggle', function(e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); var $toggle = $(this); var $node = $toggle.closest('.kundenkarte-tree-node'); var $children = $node.children('.kundenkarte-tree-children'); $toggle.toggleClass('collapsed'); $children.toggleClass('collapsed'); }); // Expand all nodes $(document).on('click', '#btn-expand-all', function(e) { e.preventDefault(); self.expandAll(); }); // Collapse all nodes $(document).on('click', '#btn-collapse-all', function(e) { e.preventDefault(); self.collapseAll(); }); // Compact mode toggle $(document).on('click', '#btn-compact-mode', function(e) { e.preventDefault(); self.toggleCompactMode(); }); // Ausgebaute Elemente ein-/ausblenden $(document).on('click', '#btn-toggle-decommissioned', function(e) { e.preventDefault(); var $btn = $(this); $btn.toggleClass('active'); var show = $btn.hasClass('active'); $btn.find('i').toggleClass('fa-eye-slash', !show).toggleClass('fa-eye', show); $('.kundenkarte-tree').toggleClass('show-decommissioned', show); }); // Ausbauen/Einbauen per Klick $(document).on('click', '.btn-toggle-decommissioned', function(e) { e.preventDefault(); e.stopPropagation(); var $btn = $(this); var anlageId = $btn.data('anlage-id'); if (!anlageId) return; // Prüfen ob Element bereits ausgebaut ist var $row = $btn.closest('.decommissioned'); if ($row.length) { // Wieder einbauen - einfache Bestätigung KundenKarte.showConfirm('Wieder einbauen', 'Element wieder als eingebaut markieren?', function() { $.post(baseUrl + '/custom/kundenkarte/ajax/anlage.php', { action: 'toggle_decommissioned', anlage_id: anlageId, token: $('input[name="token"]').val() || '' }, function(res) { if (res.success) { location.reload(); } else { alert(res.error || 'Fehler'); } }, 'json'); }); } else { // Ausbauen - Dialog mit Datumsfeld KundenKarte.showDecommissionDialog(anlageId); } }); // In compact mode, click on item to expand/show details $(document).on('click', '.kundenkarte-tree.compact-mode .kundenkarte-tree-item', function(e) { // Don't trigger on action buttons or toggle if ($(e.target).closest('.kundenkarte-tree-actions, .kundenkarte-tree-toggle, .kundenkarte-tree-files').length) { return; } $(this).toggleClass('expanded'); }); // Hover tooltip on ICON only - show after delay $(document).on('mouseenter', '.kundenkarte-tooltip-trigger', function(e) { var $trigger = $(this); var anlageId = $trigger.data('anlage-id'); if (!anlageId) return; // Cancel any pending hide clearTimeout(self.hideTimeout); self.hideTimeout = null; self.currentItem = $trigger; self.tooltipTimeout = setTimeout(function() { self.showTooltip($trigger, anlageId); }, 300); }); // Hide tooltip when leaving icon $(document).on('mouseleave', '.kundenkarte-tooltip-trigger', function() { clearTimeout(self.tooltipTimeout); self.tooltipTimeout = null; self.currentItem = null; // Hide after short delay (allows moving to tooltip) self.hideTimeout = setTimeout(function() { self.hideTooltip(); }, 100); }); // Images tooltip on hover $(document).on('mouseenter', '.kundenkarte-images-trigger', function(e) { var $trigger = $(this); var anlageId = $trigger.data('anlage-id'); if (!anlageId) return; clearTimeout(self.hideTimeout); self.hideTimeout = null; self.tooltipTimeout = setTimeout(function() { self.showImagesPopup($trigger, anlageId); }, 300); }); $(document).on('mouseleave', '.kundenkarte-images-trigger', function() { clearTimeout(self.tooltipTimeout); self.tooltipTimeout = null; self.hideTimeout = setTimeout(function() { self.hideTooltip(); }, 100); }); // Documents tooltip on hover $(document).on('mouseenter', '.kundenkarte-docs-trigger', function(e) { var $trigger = $(this); var anlageId = $trigger.data('anlage-id'); if (!anlageId) return; clearTimeout(self.hideTimeout); self.hideTimeout = null; self.tooltipTimeout = setTimeout(function() { self.showDocsPopup($trigger, anlageId); }, 300); }); $(document).on('mouseleave', '.kundenkarte-docs-trigger', function() { clearTimeout(self.tooltipTimeout); self.tooltipTimeout = null; self.hideTimeout = setTimeout(function() { self.hideTooltip(); }, 100); }); // File badge tooltip on hover (combined images + documents) $(document).on('mouseenter', '.kundenkarte-tree-file-badge', function(e) { var $trigger = $(this); var anlageId = $trigger.data('anlage-id'); if (!anlageId) return; clearTimeout(self.hideTimeout); self.hideTimeout = null; self.tooltipTimeout = setTimeout(function() { self.showFilePreview($trigger, anlageId); }, 300); }); $(document).on('mouseleave', '.kundenkarte-tree-file-badge', function() { clearTimeout(self.tooltipTimeout); self.tooltipTimeout = null; self.hideTimeout = setTimeout(function() { self.hideTooltip(); }, 100); }); // Keep tooltip visible when hovering over it $(document).on('mouseenter', '#kundenkarte-tooltip', function() { clearTimeout(self.hideTimeout); self.hideTimeout = null; }); // Hide when leaving tooltip $(document).on('mouseleave', '#kundenkarte-tooltip', function() { self.hideTooltip(); }); // Select item $(document).on('click', '.kundenkarte-tree-item', function(e) { if ($(e.target).closest('.kundenkarte-tree-toggle, .kundenkarte-tree-actions, .kundenkarte-tree-files').length) { return; } $('.kundenkarte-tree-item').removeClass('selected'); $(this).addClass('selected'); var anlageId = $(this).data('anlage-id'); if (anlageId) { $(document).trigger('kundenkarte:element:selected', [anlageId]); } }); }, showTooltip: function($item, anlageId) { var self = this; // Get tooltip data from data attribute (faster than AJAX) var tooltipDataStr = $item.attr('data-tooltip'); if (!tooltipDataStr) { // No tooltip data available - silently return return; } var data; try { data = JSON.parse(tooltipDataStr); } catch(e) { console.error('Failed to parse tooltip JSON:', e, tooltipDataStr); return; } var html = self.buildTooltipHtml(data); var $tooltip = $('#kundenkarte-tooltip'); if (!$tooltip.length) { $tooltip = $('
'); $('body').append($tooltip); } $tooltip.html(html); // Position tooltip var offset = $item.offset(); var itemWidth = $item.outerWidth(); var windowWidth = $(window).width(); var scrollTop = $(window).scrollTop(); // First show to calculate width $tooltip.css({ visibility: 'hidden', display: 'block' }); var tooltipWidth = $tooltip.outerWidth(); var tooltipHeight = $tooltip.outerHeight(); $tooltip.css({ visibility: '', display: '' }); var left = offset.left + itemWidth + 10; if (left + tooltipWidth > windowWidth - 20) { left = offset.left - tooltipWidth - 10; } if (left < 10) { left = 10; } var top = offset.top; // Prevent tooltip from going below viewport if (top + tooltipHeight > scrollTop + $(window).height() - 20) { top = scrollTop + $(window).height() - tooltipHeight - 20; } $tooltip.css({ top: top, left: left }).addClass('visible').show(); self.currentTooltip = $tooltip; }, hideTooltip: function() { clearTimeout(this.hideTimeout); this.hideTimeout = null; var $tooltip = $('#kundenkarte-tooltip'); if ($tooltip.length) { $tooltip.removeClass('visible').hide(); } this.currentTooltip = null; }, showImagesPopup: function($trigger, anlageId) { var self = this; // Load images via AJAX - use absolute path $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/anlage_images.php', data: { anlage_id: anlageId }, dataType: 'json', success: function(response) { if (!response.images || response.images.length === 0) { return; } var html = '
'; html += '
'; for (var i = 0; i < response.images.length; i++) { var img = response.images[i]; html += ''; html += '' + self.escapeHtml(img.name) + ''; html += ''; } html += '
'; html += '
'; var $tooltip = $('#kundenkarte-tooltip'); if (!$tooltip.length) { $tooltip = $('
'); $('body').append($tooltip); } $tooltip.html(html); // Position tooltip var offset = $trigger.offset(); var windowWidth = $(window).width(); var scrollTop = $(window).scrollTop(); $tooltip.css({ visibility: 'hidden', display: 'block' }); var tooltipWidth = $tooltip.outerWidth(); var tooltipHeight = $tooltip.outerHeight(); $tooltip.css({ visibility: '', display: '' }); var left = offset.left + $trigger.outerWidth() + 10; if (left + tooltipWidth > windowWidth - 20) { left = offset.left - tooltipWidth - 10; } if (left < 10) left = 10; var top = offset.top; if (top + tooltipHeight > scrollTop + $(window).height() - 20) { top = scrollTop + $(window).height() - tooltipHeight - 20; } $tooltip.css({ top: top, left: left }).addClass('visible').show(); self.currentTooltip = $tooltip; } }); }, showDocsPopup: function($trigger, anlageId) { var self = this; // Load documents via AJAX $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/anlage_docs.php', data: { anlage_id: anlageId }, dataType: 'json', success: function(response) { if (!response.docs || response.docs.length === 0) { return; } // Visual document cards with icons var html = '
'; html += '
'; for (var i = 0; i < response.docs.length; i++) { var doc = response.docs[i]; var iconClass = doc.type === 'pdf' ? 'fa-file-pdf-o' : 'fa-file-text-o'; var iconColor = doc.type === 'pdf' ? '#e74c3c' : '#f39c12'; html += ''; html += '
'; html += ''; html += '
'; html += '
' + self.escapeHtml(doc.name) + '
'; html += '
'; } html += '
'; html += '
'; var $tooltip = $('#kundenkarte-tooltip'); if (!$tooltip.length) { $tooltip = $('
'); $('body').append($tooltip); } $tooltip.html(html); // Position tooltip var offset = $trigger.offset(); var windowWidth = $(window).width(); var scrollTop = $(window).scrollTop(); $tooltip.css({ visibility: 'hidden', display: 'block' }); var tooltipWidth = $tooltip.outerWidth(); var tooltipHeight = $tooltip.outerHeight(); $tooltip.css({ visibility: '', display: '' }); var left = offset.left + $trigger.outerWidth() + 10; if (left + tooltipWidth > windowWidth - 20) { left = offset.left - tooltipWidth - 10; } if (left < 10) left = 10; var top = offset.top; if (top + tooltipHeight > scrollTop + $(window).height() - 20) { top = scrollTop + $(window).height() - tooltipHeight - 20; } $tooltip.css({ top: top, left: left }).addClass('visible').show(); self.currentTooltip = $tooltip; } }); }, showFilePreview: function($trigger, anlageId) { var self = this; // Load all files via AJAX $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/file_preview.php', data: { anlage_id: anlageId }, dataType: 'json', success: function(response) { if ((!response.images || response.images.length === 0) && (!response.documents || response.documents.length === 0)) { return; } var html = '
'; // Images section with thumbnails if (response.images && response.images.length > 0) { html += '
'; html += '
Bilder (' + response.images.length + ')
'; html += '
'; for (var i = 0; i < response.images.length && i < 6; i++) { var img = response.images[i]; var pinnedClass = img.is_pinned ? ' is-pinned' : ''; var coverClass = img.is_cover ? ' is-cover' : ''; html += ''; html += '' + self.escapeHtml(img.name) + ''; if (img.is_pinned) { html += ''; } html += ''; } if (response.images.length > 6) { html += '+' + (response.images.length - 6) + ''; } html += '
'; html += '
'; } // Documents section with icons if (response.documents && response.documents.length > 0) { html += '
'; html += '
Dokumente (' + response.documents.length + ')
'; html += '
'; for (var j = 0; j < response.documents.length && j < 5; j++) { var doc = response.documents[j]; var docPinnedClass = doc.is_pinned ? ' is-pinned' : ''; html += ''; html += ''; html += '' + self.escapeHtml(doc.name) + ''; if (doc.is_pinned) { html += ''; } html += ''; } if (response.documents.length > 5) { html += '
+' + (response.documents.length - 5) + ' weitere
'; } html += '
'; html += '
'; } html += '
'; var $tooltip = $('#kundenkarte-tooltip'); if (!$tooltip.length) { $tooltip = $('
'); $('body').append($tooltip); } $tooltip.html(html); // Position tooltip var offset = $trigger.offset(); var windowWidth = $(window).width(); var scrollTop = $(window).scrollTop(); $tooltip.css({ visibility: 'hidden', display: 'block' }); var tooltipWidth = $tooltip.outerWidth(); var tooltipHeight = $tooltip.outerHeight(); $tooltip.css({ visibility: '', display: '' }); var left = offset.left + $trigger.outerWidth() + 10; if (left + tooltipWidth > windowWidth - 20) { left = offset.left - tooltipWidth - 10; } if (left < 10) left = 10; var top = offset.top; if (top + tooltipHeight > scrollTop + $(window).height() - 20) { top = scrollTop + $(window).height() - tooltipHeight - 20; } $tooltip.css({ top: top, left: left }).addClass('visible').show(); self.currentTooltip = $tooltip; } }); }, buildTooltipHtml: function(data) { var html = '
'; html += ''; html += '
'; html += '
' + this.escapeHtml(data.label || '') + '
'; html += '
' + this.escapeHtml(data.type || data.type_label || '') + '
'; html += '
'; html += '
'; // Dynamic fields only (from PHP data-tooltip attribute) if (data.fields) { for (var key in data.fields) { if (data.fields.hasOwnProperty(key)) { var field = data.fields[key]; // Handle header fields as section titles (must span both grid columns) if (field.type === 'header') { html += '' + this.escapeHtml(field.label) + ''; } else if (field.value) { html += '' + this.escapeHtml(field.label) + ':'; html += '' + this.escapeHtml(field.value) + ''; } } } } html += '
'; // Notes (note_html is already sanitized and formatted with
by PHP) if (data.note_html) { html += '
'; html += '
' + data.note_html; html += '
'; } // Images (from AJAX) if (data.images && data.images.length > 0) { html += '
'; for (var i = 0; i < Math.min(data.images.length, 4); i++) { html += ''; } html += '
'; } return html; }, escapeHtml: function(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, escapeHtmlPreservingBreaks: function(text) { if (!text) return ''; // Convert
to newlines first (for old data with
tags) text = text.replace(//gi, '\n'); return this.escapeHtml(text); }, refresh: function(socId, systemId) { var $container = $('.kundenkarte-tree[data-system="' + systemId + '"]'); if (!$container.length) return; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/anlage_tree.php', data: { socid: socId, system: systemId }, success: function(html) { $container.html(html); } }); }, // Drag & Drop Sortierung initialisieren initDragDrop: function() { var self = this; this.draggedNode = null; $(document).on('mousedown', '.kundenkarte-tree-item', function(e) { // Nicht bei Klick auf Links, Buttons oder Toggle if ($(e.target).closest('a, button, .kundenkarte-tree-toggle').length) return; var $item = $(this); var $node = $item.closest('.kundenkarte-tree-node'); // Nur wenn Schreibberechtigung (Actions vorhanden) if (!$item.find('.kundenkarte-tree-actions').length) return; self.draggedNode = $node; self.dragStartY = e.pageY; self.isDragging = false; $(document).on('mousemove.treeDrag', function(e) { // Erst ab 5px Bewegung als Drag werten if (!self.isDragging && Math.abs(e.pageY - self.dragStartY) > 5) { self.isDragging = true; self.draggedNode.addClass('kundenkarte-dragging'); $('body').addClass('kundenkarte-drag-active'); } if (self.isDragging) { self.handleDragOver(e); } }); $(document).on('mouseup.treeDrag', function(e) { $(document).off('mousemove.treeDrag mouseup.treeDrag'); if (self.isDragging) { self.handleDrop(); } self.draggedNode.removeClass('kundenkarte-dragging'); $('body').removeClass('kundenkarte-drag-active'); $('.kundenkarte-drag-above, .kundenkarte-drag-below').removeClass('kundenkarte-drag-above kundenkarte-drag-below'); self.draggedNode = null; self.isDragging = false; self.dropTarget = null; }); }); }, handleDragOver: function(e) { var self = this; // Alle Markierungen entfernen $('.kundenkarte-drag-above, .kundenkarte-drag-below').removeClass('kundenkarte-drag-above kundenkarte-drag-below'); // Element unter dem Mauszeiger finden var $target = $(e.target).closest('.kundenkarte-tree-node'); if (!$target.length || $target.is(self.draggedNode)) return; // Nur Geschwister erlauben (gleicher Parent-Container) var $dragParent = self.draggedNode.parent(); var $targetParent = $target.parent(); if (!$dragParent.is($targetParent)) return; // Position bestimmen: obere oder untere Hälfte des Ziels var targetRect = $target.children('.kundenkarte-tree-item')[0].getBoundingClientRect(); var midY = targetRect.top + targetRect.height / 2; if (e.clientY < midY) { $target.addClass('kundenkarte-drag-above'); self.dropTarget = { node: $target, position: 'before' }; } else { $target.addClass('kundenkarte-drag-below'); self.dropTarget = { node: $target, position: 'after' }; } }, handleDrop: function() { var self = this; if (!self.dropTarget) return; var $target = self.dropTarget.node; var position = self.dropTarget.position; // DOM-Reihenfolge aktualisieren if (position === 'before') { self.draggedNode.insertBefore($target); } else { self.draggedNode.insertAfter($target); } // Neue Reihenfolge der Geschwister sammeln var $parent = self.draggedNode.parent(); var ids = []; $parent.children('.kundenkarte-tree-node').each(function() { var id = $(this).children('.kundenkarte-tree-item').data('anlage-id'); if (id) ids.push(id); }); // Per AJAX speichern var baseUrl = $('meta[name="dolibarr-baseurl"]').attr('content') || ''; $.ajax({ type: 'POST', url: baseUrl + '/custom/kundenkarte/ajax/anlage.php', data: { action: 'reorder', token: $('input[name="token"]').val() || '', 'ids[]': ids }, dataType: 'json' }); }, expandAll: function() { $('.kundenkarte-tree-toggle').removeClass('collapsed'); $('.kundenkarte-tree-children').removeClass('collapsed'); }, collapseAll: function() { $('.kundenkarte-tree-toggle').addClass('collapsed'); $('.kundenkarte-tree-children').addClass('collapsed'); }, toggleCompactMode: function() { var $tree = $('.kundenkarte-tree'); var $btn = $('#btn-compact-mode'); $tree.toggleClass('compact-mode'); $btn.toggleClass('active'); if ($tree.hasClass('compact-mode')) { $btn.find('span').text('Normal'); $btn.find('i').removeClass('fa-compress').addClass('fa-expand'); // Remove any expanded items $('.kundenkarte-tree-item.expanded').removeClass('expanded'); // Store preference localStorage.setItem('kundenkarte_compact_mode', '1'); } else { $btn.find('span').text('Kompakt'); $btn.find('i').removeClass('fa-expand').addClass('fa-compress'); localStorage.removeItem('kundenkarte_compact_mode'); } }, initCompactMode: function() { // Check localStorage for saved preference if (localStorage.getItem('kundenkarte_compact_mode') === '1') { this.toggleCompactMode(); } // Auto-enable on mobile if (window.innerWidth <= 768 && !localStorage.getItem('kundenkarte_compact_mode_manual')) { if (!$('.kundenkarte-tree').hasClass('compact-mode')) { this.toggleCompactMode(); } } } }; /** * Favorite Products Component */ KundenKarte.Favorites = { init: function() { this.bindEvents(); }, bindEvents: function() { // Select all checkbox $(document).on('change', '#kundenkarte-select-all', function() { var checked = $(this).prop('checked'); $('.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]').prop('checked', checked); KundenKarte.Favorites.updateGenerateButton(); }); // Individual checkbox $(document).on('change', '.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]', function() { KundenKarte.Favorites.updateGenerateButton(); }); // Save button click $(document).on('click', '.kundenkarte-qty-save', function(e) { e.preventDefault(); var $btn = $(this); var favId = $btn.data('fav-id'); var $input = $('input.kundenkarte-favorites-qty[data-fav-id="' + favId + '"]'); var qtyStr = $input.val().replace(',', '.'); var qty = parseFloat(qtyStr); if (!isNaN(qty) && qty > 0) { // Limit to 2 decimal places qty = Math.round(qty * 100) / 100; // Format nicely var display = (qty % 1 === 0) ? qty.toString() : qty.toFixed(2).replace(/\.?0+$/, ''); $input.val(display); // Visual feedback $btn.prop('disabled', true); $btn.find('i').removeClass('fa-save').addClass('fa-spinner fa-spin'); $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/favorite_update.php', method: 'POST', data: { id: favId, qty: qty, token: $('input[name="token"]').val() }, success: function() { $btn.find('i').removeClass('fa-spinner fa-spin').addClass('fa-check').css('color', '#0a0'); setTimeout(function() { $btn.find('i').removeClass('fa-check').addClass('fa-save').css('color', ''); $btn.prop('disabled', false); }, 1500); }, error: function() { $btn.find('i').removeClass('fa-spinner fa-spin').addClass('fa-exclamation-triangle').css('color', '#c00'); setTimeout(function() { $btn.find('i').removeClass('fa-exclamation-triangle').addClass('fa-save').css('color', ''); $btn.prop('disabled', false); }, 2000); } }); } }); }, updateGenerateButton: function() { var count = $('.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]:checked').length; var $btn = $('#btn-generate-order'); if (count > 0) { $btn.prop('disabled', false).text($btn.data('text').replace('%d', count)); } else { $btn.prop('disabled', true).text($btn.data('text-none')); } } }; /** * System Tabs Component */ KundenKarte.SystemTabs = { init: function() { this.bindEvents(); }, bindEvents: function() { $(document).on('click', '.kundenkarte-system-tab:not(.active):not(.kundenkarte-system-tab-add)', function() { var systemId = $(this).data('system'); KundenKarte.SystemTabs.switchTo(systemId); }); }, switchTo: function(systemId) { $('.kundenkarte-system-tab').removeClass('active'); $('.kundenkarte-system-tab[data-system="' + systemId + '"]').addClass('active'); $('.kundenkarte-system-content').hide(); $('.kundenkarte-system-content[data-system="' + systemId + '"]').show(); // Update URL without reload var url = new URL(window.location.href); url.searchParams.set('system', systemId); window.history.replaceState({}, '', url); } }; /** * Icon Picker Component with Custom Icon Upload */ KundenKarte.IconPicker = { // Common FontAwesome icons for installations/technical use icons: [ // Electrical 'fa-bolt', 'fa-plug', 'fa-power-off', 'fa-charging-station', 'fa-battery-full', 'fa-battery-half', 'fa-car-battery', 'fa-solar-panel', 'fa-sun', 'fa-lightbulb', 'fa-toggle-on', 'fa-toggle-off', // Network/Internet 'fa-wifi', 'fa-network-wired', 'fa-server', 'fa-database', 'fa-hdd', 'fa-ethernet', 'fa-broadcast-tower', 'fa-satellite-dish', 'fa-satellite', 'fa-signal', 'fa-rss', // TV/Media 'fa-tv', 'fa-play-circle', 'fa-video', 'fa-film', 'fa-podcast', 'fa-music', // Temperature/Climate 'fa-thermometer-half', 'fa-temperature-high', 'fa-temperature-low', 'fa-fire', 'fa-fire-alt', 'fa-snowflake', 'fa-wind', 'fa-fan', 'fa-air-freshener', // Building/Structure 'fa-home', 'fa-building', 'fa-warehouse', 'fa-door-open', 'fa-door-closed', 'fa-archway', // Devices/Hardware 'fa-microchip', 'fa-memory', 'fa-sim-card', 'fa-sd-card', 'fa-usb', 'fa-desktop', 'fa-laptop', 'fa-mobile-alt', 'fa-tablet-alt', 'fa-keyboard', 'fa-print', 'fa-fax', // Security 'fa-shield-alt', 'fa-lock', 'fa-unlock', 'fa-key', 'fa-fingerprint', 'fa-eye', 'fa-video', 'fa-bell', 'fa-exclamation-triangle', 'fa-user-shield', // Objects 'fa-cube', 'fa-cubes', 'fa-box', 'fa-boxes', 'fa-archive', 'fa-toolbox', 'fa-tools', 'fa-wrench', 'fa-cog', 'fa-cogs', 'fa-sliders-h', // Layout 'fa-th', 'fa-th-large', 'fa-th-list', 'fa-grip-horizontal', 'fa-grip-vertical', 'fa-bars', 'fa-stream', 'fa-layer-group', 'fa-project-diagram', 'fa-share-alt', 'fa-sitemap', // Arrows/Direction 'fa-exchange-alt', 'fa-arrows-alt', 'fa-expand', 'fa-compress', 'fa-random', // Misc 'fa-tachometer-alt', 'fa-chart-line', 'fa-chart-bar', 'fa-chart-pie', 'fa-chart-area', 'fa-clock', 'fa-calendar', 'fa-tag', 'fa-tags', 'fa-bookmark', 'fa-star', 'fa-heart', 'fa-check', 'fa-times', 'fa-plus', 'fa-minus', 'fa-info-circle', 'fa-question-circle', 'fa-dot-circle', 'fa-circle', 'fa-square', 'fa-adjust' ], customIcons: [], currentInput: null, currentTab: 'fontawesome', init: function() { this.createModal(); this.bindEvents(); }, createModal: function() { if ($('#kundenkarte-icon-picker-modal').length) return; var self = this; var html = '
'; html += '
'; html += '
'; html += '

Icon auswählen

'; html += '×'; html += '
'; html += '
'; // Tabs html += '
'; html += ''; html += ''; html += '
'; // Font Awesome Tab Content html += '
'; html += ''; html += '
'; for (var i = 0; i < this.icons.length; i++) { html += '
'; html += ''; html += '
'; } html += '
'; html += '
'; // Custom Icons Tab Content html += ''; html += '
'; html += '
'; html += '
'; $('body').append(html); }, bindEvents: function() { var self = this; // Open picker $(document).on('click', '.kundenkarte-icon-picker-btn', function(e) { e.preventDefault(); var inputName = $(this).data('input'); self.currentInput = $('input[name="' + inputName + '"]'); self.open(); }); // Close modal $(document).on('click', '.kundenkarte-modal-close, #kundenkarte-icon-picker-modal', function(e) { if (e.target === this || $(e.target).hasClass('kundenkarte-modal-close')) { self.close(); } }); // Tab switching $(document).on('click', '.kundenkarte-icon-tab', function() { var tab = $(this).data('tab'); self.switchTab(tab); }); // Select FA icon $(document).on('click', '.kundenkarte-icon-item[data-type="fa"]', function() { var icon = $(this).data('icon'); self.selectIcon(icon, 'fa'); }); // Select custom icon $(document).on('click', '.kundenkarte-icon-item[data-type="custom"]', function() { var iconUrl = $(this).data('icon'); self.selectIcon(iconUrl, 'custom'); }); // Search filter (FA only) $(document).on('input', '#kundenkarte-icon-search', function() { var search = $(this).val().toLowerCase(); $('#kundenkarte-fa-icons .kundenkarte-icon-item').each(function() { var icon = $(this).data('icon').toLowerCase(); $(this).toggle(icon.indexOf(search) > -1); }); }); // Upload button click $(document).on('click', '#kundenkarte-icon-upload-btn', function() { $('#kundenkarte-icon-upload').click(); }); // File selected $(document).on('change', '#kundenkarte-icon-upload', function() { var file = this.files[0]; if (file) { self.uploadIcon(file); } }); // Delete custom icon $(document).on('click', '.kundenkarte-icon-delete', function(e) { e.stopPropagation(); var filename = $(this).data('filename'); self.showDeleteConfirm(filename); }); // ESC key to close $(document).on('keydown', function(e) { if (e.key === 'Escape') { self.close(); } }); }, switchTab: function(tab) { this.currentTab = tab; $('.kundenkarte-icon-tab').removeClass('active'); $('.kundenkarte-icon-tab[data-tab="' + tab + '"]').addClass('active'); $('.kundenkarte-icon-tab-content').hide(); $('.kundenkarte-icon-tab-content[data-tab="' + tab + '"]').show(); if (tab === 'custom') { this.loadCustomIcons(); } }, loadCustomIcons: function() { var self = this; var $grid = $('#kundenkarte-custom-icons'); $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', data: { action: 'list' }, dataType: 'json', success: function(response) { $grid.empty(); if (response.icons && response.icons.length > 0) { self.customIcons = response.icons; for (var i = 0; i < response.icons.length; i++) { var icon = response.icons[i]; var html = '
'; html += '' + icon.name + ''; html += ''; html += '
'; $grid.append(html); } } else { $grid.html('

Noch keine eigenen Icons hochgeladen.

'); } }, error: function() { $grid.html('

Fehler beim Laden der Icons.

'); } }); }, uploadIcon: function(file) { var self = this; var formData = new FormData(); formData.append('action', 'upload'); formData.append('icon', file); $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', method: 'POST', data: formData, processData: false, contentType: false, dataType: 'json', success: function(response) { if (response.success) { self.loadCustomIcons(); // Auto-select uploaded icon setTimeout(function() { self.selectIcon(response.icon.url, 'custom'); }, 500); } else { KundenKarte.showAlert('Fehler', response.error || 'Unbekannter Fehler'); } }, error: function(xhr) { var msg = 'Upload fehlgeschlagen'; try { var resp = JSON.parse(xhr.responseText); msg = resp.error || msg; } catch(e) {} KundenKarte.showAlert('Fehler', msg); } }); // Reset input $('#kundenkarte-icon-upload').val(''); }, deleteIcon: function(filename) { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', method: 'POST', data: { action: 'delete', filename: filename }, dataType: 'json', success: function(response) { if (response.success) { self.loadCustomIcons(); } else { KundenKarte.showAlert('Fehler', response.error || 'Löschen fehlgeschlagen'); } } }); }, showDeleteConfirm: function(filename) { var self = this; // Remove any existing confirm dialog $('#kundenkarte-delete-confirm').remove(); // Create Dolibarr-style confirmation dialog var html = '
'; html += '
'; // Header (Dolibarr style) html += '
'; html += 'Bestätigung'; html += '
'; // Body html += '
'; html += '

Möchten Sie dieses Icon wirklich löschen?

'; html += '

' + this.escapeHtml(filename) + '

'; html += '
'; // Footer with buttons (Dolibarr style) html += ''; html += '
'; html += '
'; $('body').append(html); // Bind events $('#kundenkarte-confirm-yes').on('click', function() { $('#kundenkarte-delete-confirm').remove(); self.deleteIcon(filename); }); $('#kundenkarte-confirm-no, #kundenkarte-delete-confirm').on('click', function(e) { if (e.target === this) { $('#kundenkarte-delete-confirm').remove(); } }); // ESC to close $(document).one('keydown.confirmDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-delete-confirm').remove(); } }); }, escapeHtml: function(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, selectIcon: function(icon, type) { if (this.currentInput) { // For custom icons, store the URL with a prefix to distinguish var value = (type === 'custom') ? 'img:' + icon : icon; this.currentInput.val(value); // Update preview var $wrapper = this.currentInput.closest('.kundenkarte-icon-picker-wrapper'); var $preview = $wrapper.find('.kundenkarte-icon-preview'); if ($preview.length) { if (type === 'custom') { $preview.html(''); } else { $preview.html(''); } } } this.close(); }, open: function() { $('#kundenkarte-icon-search').val(''); $('#kundenkarte-fa-icons .kundenkarte-icon-item').show(); this.switchTab('fontawesome'); $('#kundenkarte-icon-picker-modal').addClass('visible'); }, close: function() { $('#kundenkarte-icon-picker-modal').removeClass('visible'); this.currentInput = null; } }; /** * Dynamic Fields Component * Loads and renders type-specific fields when creating/editing anlagen */ KundenKarte.DynamicFields = { init: function() { var self = this; var $typeSelect = $('select[name="fk_anlage_type"]'); var $container = $('#dynamic_fields'); if (!$typeSelect.length || !$container.length) return; // Load fields when type changes $typeSelect.on('change', function() { self.loadFields($(this).val()); }); // Load initial fields if type is already selected if ($typeSelect.val()) { self.loadFields($typeSelect.val()); } }, loadFields: function(typeId) { var self = this; var $container = $('#dynamic_fields'); if (!typeId) { $container.html(''); return; } // Store current type ID for autocomplete this.currentTypeId = typeId; // Get anlage_id if editing or copying var anlageId = $('input[name="anlage_id"]').val() || $('input[name="copy_from"]').val() || 0; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/type_fields.php', data: { type_id: typeId, anlage_id: anlageId }, dataType: 'json', success: function(data) { if (data.fields && data.fields.length > 0) { var html = ''; data.fields.forEach(function(field) { if (field.type === 'header') { // Header row spans both columns with styling html += '' + KundenKarte.DynamicFields.escapeHtml(field.label) + ''; } else { html += '' + KundenKarte.DynamicFields.escapeHtml(field.label); if (field.required) html += ' *'; html += ''; html += KundenKarte.DynamicFields.renderField(field); html += ''; } }); $container.html(html); } else { $container.html(''); } } }); }, currentTypeId: 0, renderField: function(field) { var name = 'field_' + field.code; var value = field.value || ''; var required = field.required ? ' required' : ''; var autocompleteClass = field.autocomplete ? ' kk-autocomplete' : ''; var autocompleteAttrs = field.autocomplete ? ' data-field-code="' + field.code + '" data-type-id="' + this.currentTypeId + '"' : ''; switch (field.type) { case 'text': return ''; case 'textarea': return ''; case 'number': var attrs = ''; if (field.options) { var opts = field.options.split('|'); opts.forEach(function(opt) { var parts = opt.split(':'); if (parts.length === 2) { attrs += ' ' + parts[0] + '="' + parts[1] + '"'; } }); } return ''; case 'select': var html = ''; return html; case 'date': var inputId = 'date_' + name.replace(/[^a-zA-Z0-9]/g, '_'); return '' + ''; case 'checkbox': var checked = (value === '1' || value === 'true' || value === 'yes') ? ' checked' : ''; return ''; default: return ''; } }, escapeHtml: function(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; /** * Equipment Component * Manages Hutschienen (DIN rail) components with SVG visualization */ KundenKarte.Equipment = { TE_WIDTH: 50, // Width of one TE in pixels (wider for more space) BLOCK_HEIGHT: 110, // Height of equipment block in pixels currentCarrierId: null, currentAnlageId: null, currentSystemId: null, draggedEquipment: null, isSaving: false, // Prevent double-clicks init: function(anlageId, systemId) { if (!anlageId) return; this.currentAnlageId = anlageId; this.currentSystemId = systemId || 0; this.bindEvents(); }, bindEvents: function() { var self = this; // Add panel button $(document).on('click', '.kundenkarte-add-panel', function(e) { e.preventDefault(); var anlageId = $(this).data('anlage-id'); self.showPanelDialog(anlageId); }); // Edit panel $(document).on('click', '.kundenkarte-panel-edit', function(e) { e.preventDefault(); e.stopPropagation(); var panelId = $(this).closest('.kundenkarte-panel').data('panel-id'); self.showPanelDialog(self.currentAnlageId, panelId); }); // Delete panel $(document).on('click', '.kundenkarte-panel-delete', function(e) { e.preventDefault(); e.stopPropagation(); var panelId = $(this).closest('.kundenkarte-panel').data('panel-id'); self.deletePanel(panelId); }); // Quick-duplicate panel (+ button next to last panel) $(document).on('click', '.kundenkarte-panel-quickadd', function(e) { e.preventDefault(); e.stopPropagation(); var panelId = $(this).data('panel-id'); self.duplicatePanel(panelId); }); // Quick-duplicate carrier (+ button below last carrier) $(document).on('click', '.kundenkarte-carrier-quickadd', function(e) { e.preventDefault(); e.stopPropagation(); var carrierId = $(this).data('carrier-id'); self.duplicateCarrier(carrierId); }); // Add carrier button $(document).on('click', '.kundenkarte-add-carrier', function(e) { e.preventDefault(); var anlageId = $(this).data('anlage-id'); var panelId = $(this).data('panel-id') || 0; self.showCarrierDialog(anlageId, null, panelId); }); // Edit carrier $(document).on('click', '.kundenkarte-carrier-edit', function(e) { e.preventDefault(); e.stopPropagation(); var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); self.showCarrierDialog(self.currentAnlageId, carrierId); }); // Delete carrier $(document).on('click', '.kundenkarte-carrier-delete', function(e) { e.preventDefault(); e.stopPropagation(); var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); self.deleteCarrier(carrierId); }); // Add equipment (click on empty slot or + button) $(document).on('click', '.kundenkarte-carrier-add-equipment, .kundenkarte-slot-empty', function(e) { e.preventDefault(); var $carrier = $(this).closest('.kundenkarte-carrier'); var carrierId = $carrier.data('carrier-id'); var position = $(this).data('position') || null; self.showEquipmentDialog(carrierId, null, position); }); // Edit equipment (click on block) $(document).on('click', '.kundenkarte-equipment-block', function(e) { e.preventDefault(); e.stopPropagation(); var equipmentId = $(this).data('equipment-id'); self.showEquipmentDialog(null, equipmentId); }); // Quick-add slot (duplicate last equipment into next free position) $(document).on('click', '.kundenkarte-slot-quickadd', function(e) { e.preventDefault(); e.stopPropagation(); var equipmentId = $(this).data('equipment-id'); self.duplicateEquipment(equipmentId); }); // Delete equipment $(document).on('click', '.kundenkarte-equipment-delete', function(e) { e.preventDefault(); e.stopPropagation(); var equipmentId = $(this).closest('.kundenkarte-equipment-block').data('equipment-id'); self.deleteEquipment(equipmentId); }); // Add output connection $(document).on('click', '.kundenkarte-add-output-btn', function(e) { e.preventDefault(); var carrierId = $(this).data('carrier-id'); self.showOutputDialog(carrierId); }); // Add rail connection $(document).on('click', '.kundenkarte-add-rail-btn', function(e) { e.preventDefault(); var carrierId = $(this).data('carrier-id'); self.showRailDialog(carrierId); }); // Click on rail to edit $(document).on('click', '.kundenkarte-rail', function(e) { e.preventDefault(); e.stopPropagation(); var connectionId = $(this).data('connection-id'); // Find carrier ID from connections container or carrier element var carrierId = $(this).closest('.kundenkarte-connections-container').data('carrier-id') || $(this).closest('.kundenkarte-carrier').data('carrier-id'); self.showEditRailDialog(connectionId, carrierId); }); // Click on output to edit $(document).on('click', '.kundenkarte-output', function(e) { e.preventDefault(); e.stopPropagation(); var connectionId = $(this).data('connection-id'); // Find carrier ID from connections container or carrier element var carrierId = $(this).closest('.kundenkarte-connections-container').data('carrier-id') || $(this).closest('.kundenkarte-carrier').data('carrier-id'); self.showEditOutputDialog(connectionId, carrierId); }); // Click on connection to delete (generic connections) $(document).on('click', '.kundenkarte-connection', function(e) { e.preventDefault(); var connectionId = $(this).data('connection-id'); self.deleteConnection(connectionId); }); // Drag & Drop events $(document).on('dragstart', '.kundenkarte-equipment-block', function(e) { self.draggedEquipment = $(this); $(this).addClass('dragging'); e.originalEvent.dataTransfer.effectAllowed = 'move'; e.originalEvent.dataTransfer.setData('text/plain', $(this).data('equipment-id')); }); $(document).on('dragend', '.kundenkarte-equipment-block', function() { $(this).removeClass('dragging'); self.draggedEquipment = null; $('.kundenkarte-slot-drop-target').removeClass('kundenkarte-slot-drop-target'); }); $(document).on('dragover', '.kundenkarte-slot-empty, .kundenkarte-carrier-slots', function(e) { e.preventDefault(); e.originalEvent.dataTransfer.dropEffect = 'move'; }); $(document).on('dragenter', '.kundenkarte-slot-empty', function(e) { e.preventDefault(); $(this).addClass('kundenkarte-slot-drop-target'); }); $(document).on('dragleave', '.kundenkarte-slot-empty', function() { $(this).removeClass('kundenkarte-slot-drop-target'); }); $(document).on('drop', '.kundenkarte-slot-empty', function(e) { e.preventDefault(); $(this).removeClass('kundenkarte-slot-drop-target'); if (self.draggedEquipment) { var equipmentId = self.draggedEquipment.data('equipment-id'); var newPosition = $(this).data('position'); self.moveEquipment(equipmentId, newPosition); } }); // Equipment type change in dialog $(document).on('change', '#equipment_type_id', function() { self.loadEquipmentTypeFields($(this).val()); }); // Hover tooltip for equipment $(document).on('mouseenter', '.kundenkarte-equipment-block', function() { var $block = $(this); var tooltipData = $block.attr('data-tooltip'); if (tooltipData) { self.showEquipmentTooltip($block, JSON.parse(tooltipData)); } }); $(document).on('mouseleave', '.kundenkarte-equipment-block', function() { self.hideEquipmentTooltip(); }); }, loadCarriers: function(anlageId) { var self = this; var $container = $('.kundenkarte-equipment-container[data-anlage-id="' + anlageId + '"]'); if (!$container.length) return; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', data: { action: 'list', anlage_id: anlageId }, dataType: 'json', success: function(response) { if (response.success) { self.renderCarriers($container, response.carriers); } } }); }, renderCarriers: function($container, carriers) { var self = this; var html = ''; if (carriers.length === 0) { html = '
Keine Hutschienen vorhanden. Klicken Sie auf "Hutschiene hinzufügen".
'; } else { carriers.forEach(function(carrier) { html += self.renderCarrier(carrier); }); } $container.find('.kundenkarte-carriers-list').html(html); }, renderCarrier: function(carrier) { var self = this; var totalWidth = carrier.total_te * this.TE_WIDTH; var html = '
'; // Header html += '
'; html += '' + this.escapeHtml(carrier.label || 'Hutschiene') + ''; html += '' + carrier.used_te + '/' + carrier.total_te + ' TE belegt'; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
'; // SVG Rail html += '
'; html += ''; // TE-Raster (ganzzahlige TEs deutlich, 0.5er subtil) for (var i = 0; i <= carrier.total_te; i++) { var x = i * this.TE_WIDTH; html += ''; if (i < carrier.total_te) { var halfX = x + this.TE_WIDTH / 2; html += ''; } } // Render equipment blocks if (carrier.equipment) { carrier.equipment.forEach(function(eq) { html += self.renderEquipmentBlock(eq); }); } html += ''; // Clickable slots overlay (for adding equipment) html += '
'; var occupiedSlots = this.getOccupiedSlots(carrier.equipment); for (var pos = 1; pos <= carrier.total_te; pos++) { if (!occupiedSlots[pos]) { var slotLeft = (pos - 1) * this.TE_WIDTH; html += '
'; } } html += '
'; html += '
'; // svg-container html += '
'; // carrier return html; }, renderEquipmentBlock: function(equipment) { var x = (equipment.position_te - 1) * this.TE_WIDTH; var width = equipment.width_te * this.TE_WIDTH; var color = equipment.block_color || equipment.type_color || '#3498db'; var label = equipment.block_label || equipment.type_label_short || ''; // Build tooltip data var tooltipData = { label: equipment.label, type: equipment.type_label, fields: equipment.field_values || {} }; var html = ''; // Label text (centered) var fontSize = width < 40 ? 11 : 14; html += ''; html += this.escapeHtml(label); html += ''; // Duplicate button (+) at the right edge html += ''; return html; }, getOccupiedSlots: function(equipment) { // Range-basierte Belegung (unterstützt Dezimal-TE wie 4.5) var slots = {}; if (equipment) { equipment.forEach(function(eq) { var start = parseFloat(eq.position_te) || 1; var width = parseFloat(eq.width_te) || 1; var end = start + width; // Ganzzahl-Slots markieren die von diesem Equipment überlappt werden for (var i = Math.floor(start); i < Math.ceil(end); i++) { slots[i] = true; } }); } return slots; }, // Panel dialog functions showPanelDialog: function(anlageId, panelId) { var self = this; if ($('#kundenkarte-panel-dialog').length) return; var isEdit = !!panelId; var title = isEdit ? 'Feld bearbeiten' : 'Feld hinzufügen'; var panelData = { label: '' }; var showDialog = function(data) { var html = '
'; html += '
'; html += '

' + title + '

'; html += '×
'; html += '
'; html += ''; html += ''; html += ''; html += '
Bezeichnung
'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-panel-dialog').addClass('visible'); $('#panel-save').on('click', function() { var label = $('input[name="panel_label"]').val(); self.savePanel(anlageId, panelId, label); }); $('#panel-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-panel-dialog').remove(); }); $(document).on('keydown.panelDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-panel-dialog').remove(); $(document).off('keydown.panelDialog'); } }); }; if (isEdit) { var $panel = $('.kundenkarte-panel[data-panel-id="' + panelId + '"]'); panelData.label = $panel.find('.kundenkarte-panel-label').text(); showDialog(panelData); } else { showDialog(panelData); } }, savePanel: function(anlageId, panelId, label) { var self = this; if (self.isSaving) return; self.isSaving = true; $('#panel-save').prop('disabled', true); var action = panelId ? 'update' : 'create'; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', method: 'POST', data: { action: action, anlage_id: anlageId, panel_id: panelId, label: label, token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { self.isSaving = false; if (response.success) { $('#kundenkarte-panel-dialog').remove(); location.reload(); } else { $('#panel-save').prop('disabled', false); KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#panel-save').prop('disabled', false); KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, deletePanel: function(panelId) { var self = this; self.showConfirmDialog('Feld löschen', 'Möchten Sie dieses Feld wirklich löschen? Alle Hutschienen und Equipment werden ebenfalls gelöscht.', function() { $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', method: 'POST', data: { action: 'delete', panel_id: panelId, token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { self.showAlertDialog('Fehler', response.error); } }, error: function() { self.showAlertDialog('Fehler', 'Netzwerkfehler'); } }); }); }, duplicatePanel: function(panelId) { $.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) { location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, duplicateCarrier: function(carrierId) { $.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) { location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, showCarrierDialog: function(anlageId, carrierId, panelId) { var self = this; // Prevent opening multiple dialogs if ($('#kundenkarte-carrier-dialog').length) return; var isEdit = !!carrierId; var title = isEdit ? 'Hutschiene bearbeiten' : 'Hutschiene hinzufügen'; panelId = panelId || 0; // Load carrier data if editing var carrierData = { label: '', total_te: 12, fk_panel: panelId }; var showDialog = function(data, panels) { var html = '
'; html += '
'; html += '

' + title + '

'; html += '×
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; // Panel dropdown (only show if panels exist) if (panels && panels.length > 0) { html += ''; html += ''; } html += '
Bezeichnung
Kapazität (TE)
Feld
'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-carrier-dialog').addClass('visible'); // Save button $('#carrier-save').on('click', function() { var label = $('input[name="carrier_label"]').val(); var totalTe = parseInt($('input[name="carrier_total_te"]').val()) || 12; var selectedPanelId = $('select[name="carrier_panel_id"]').val() || 0; self.saveCarrier(anlageId, carrierId, label, totalTe, selectedPanelId); }); // Close $('#carrier-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-carrier-dialog').remove(); }); $(document).on('keydown.carrierDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-carrier-dialog').remove(); $(document).off('keydown.carrierDialog'); } }); }; // Load panels for the anlage var loadPanelsAndShow = function() { $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', data: { action: 'list', anlage_id: self.currentAnlageId }, dataType: 'json', success: function(response) { var panels = response.success ? response.panels : []; showDialog(carrierData, panels); }, error: function() { showDialog(carrierData, []); } }); }; if (isEdit) { // Fetch existing carrier data via AJAX $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', data: { action: 'get', carrier_id: carrierId }, dataType: 'json', success: function(response) { if (response.success && response.carrier) { carrierData.label = response.carrier.label; carrierData.total_te = response.carrier.total_te; carrierData.fk_panel = response.carrier.fk_panel || 0; } loadPanelsAndShow(); }, error: function() { // Fallback to DOM data var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); carrierData.label = $carrier.find('.kundenkarte-carrier-label').text(); var infoText = $carrier.find('.kundenkarte-carrier-info').text(); var match = infoText.match(/\/(\d+)/); if (match) carrierData.total_te = parseInt(match[1]); loadPanelsAndShow(); } }); } else { carrierData.fk_panel = panelId; loadPanelsAndShow(); } }, saveCarrier: function(anlageId, carrierId, label, totalTe, panelId) { var self = this; // Prevent double-click if (self.isSaving) return; self.isSaving = true; $('#carrier-save').prop('disabled', true); var action = carrierId ? 'update' : 'create'; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', method: 'POST', data: { action: action, anlage_id: anlageId, carrier_id: carrierId, panel_id: panelId || 0, label: label, total_te: totalTe, token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { self.isSaving = false; if (response.success) { $('#kundenkarte-carrier-dialog').remove(); // Reload page to get fresh PHP-rendered carriers location.reload(); } else { $('#carrier-save').prop('disabled', false); KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#carrier-save').prop('disabled', false); KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, deleteCarrier: function(carrierId) { var self = this; self.showConfirmDialog('Hutschiene löschen', 'Möchten Sie diese Hutschiene wirklich löschen? Alle Equipment darauf wird ebenfalls gelöscht.', function() { $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', method: 'POST', data: { action: 'delete', carrier_id: carrierId, token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { self.showAlertDialog('Fehler', response.error); } } }); }); }, showEquipmentDialog: function(carrierId, equipmentId, position) { var self = this; // Prevent opening multiple dialogs if ($('#kundenkarte-equipment-dialog').length) return; var isEdit = !!equipmentId; var title = isEdit ? 'Equipment bearbeiten' : 'Equipment hinzufügen'; // First, load equipment types $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_type_fields.php', data: { type_id: 0 }, // Get types list dataType: 'json', success: function() { self.renderEquipmentDialog(carrierId, equipmentId, position, title, isEdit); } }); }, renderEquipmentDialog: function(carrierId, equipmentId, position, title, isEdit) { var self = this; var html = '
'; html += '
'; html += '

' + title + '

'; html += '×
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
Typ *
Bezeichnung
'; html += ' FI/RCD-Zuordnung
Schutzeinrichtung
Schutzbezeichnung
'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-equipment-dialog').addClass('visible'); // Load equipment types into select self.loadEquipmentTypes(equipmentId); // Save button $('#equipment-save').on('click', function() { self.saveEquipment(); }); // Delete button $('#equipment-delete').on('click', function() { $('#kundenkarte-equipment-dialog').remove(); self.showConfirmDialog('Equipment löschen', 'Möchten Sie dieses Equipment wirklich löschen?', function() { self.deleteEquipment(equipmentId); }); }); // Close $('#equipment-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-equipment-dialog').remove(); }); $(document).on('keydown.equipmentDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-equipment-dialog').remove(); $(document).off('keydown.equipmentDialog'); } }); }, loadEquipmentTypes: function(equipmentId) { var self = this; // Get equipment types from admin config (stored in page data or via AJAX) $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', data: { action: 'get_types', system_id: self.currentSystemId }, dataType: 'json', success: function(response) { if (response.types) { var $select = $('#equipment_type_id'); // Clear existing options except first placeholder $select.find('option:not(:first)').remove(); response.types.forEach(function(type) { $select.append(''); }); // Load protection devices self.loadProtectionDevices(); // If editing, load equipment data if (equipmentId) { self.loadEquipmentData(equipmentId); } } } }); }, loadProtectionDevices: function() { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', data: { action: 'get_protection_devices', anlage_id: self.currentAnlageId }, dataType: 'json', success: function(response) { if (response.success && response.devices) { var $select = $('#equipment_fk_protection'); $select.find('option:not(:first)').remove(); response.devices.forEach(function(device) { $select.append(''); }); } } }); }, loadEquipmentData: function(equipmentId) { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', data: { action: 'get', equipment_id: equipmentId }, dataType: 'json', success: function(response) { if (response.equipment) { var eq = response.equipment; $('input[name="equipment_carrier_id"]').val(eq.fk_carrier); $('input[name="equipment_label"]').val(eq.label); $('input[name="equipment_position"]').val(eq.position_te); $('#equipment_type_id').val(eq.type_id).trigger('change'); // Protection fields if (eq.fk_protection) { $('#equipment_fk_protection').val(eq.fk_protection); } if (eq.protection_label) { $('input[name="equipment_protection_label"]').val(eq.protection_label); } } } }); }, loadEquipmentTypeFields: function(typeId) { var self = this; var $container = $('#equipment-dynamic-fields'); var equipmentId = $('input[name="equipment_id"]').val(); if (!typeId) { $container.html(''); return; } $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_type_fields.php', data: { type_id: typeId, equipment_id: equipmentId }, dataType: 'json', success: function(response) { if (response.success && response.fields) { var html = ''; response.fields.forEach(function(field) { html += '' + self.escapeHtml(field.label); if (field.required) html += ' *'; html += ''; html += self.renderEquipmentField(field); html += ''; }); $container.html(html); } else { $container.html(''); } } }); }, renderEquipmentField: function(field) { var name = 'eq_field_' + field.code; var value = field.value || ''; var required = field.required ? ' required' : ''; switch (field.type) { case 'select': var html = ''; return html; case 'number': return ''; default: return ''; } }, saveEquipment: function() { var self = this; // Prevent double-click if (self.isSaving) return; var carrierId = $('input[name="equipment_carrier_id"]').val(); var equipmentId = $('input[name="equipment_id"]').val(); var typeId = $('#equipment_type_id').val(); var position = $('input[name="equipment_position"]').val(); var label = $('input[name="equipment_label"]').val(); var fkProtection = $('#equipment_fk_protection').val(); var protectionLabel = $('input[name="equipment_protection_label"]').val(); if (!typeId) { KundenKarte.showAlert('Hinweis', 'Bitte wählen Sie einen Typ.'); return; } self.isSaving = true; $('#equipment-save').prop('disabled', true); // Collect field values var fieldValues = {}; $('#equipment-dynamic-fields input, #equipment-dynamic-fields select').each(function() { var name = $(this).attr('name'); if (name && name.startsWith('eq_field_')) { var code = name.replace('eq_field_', ''); fieldValues[code] = $(this).val(); } }); var action = equipmentId ? 'update' : 'create'; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', method: 'POST', data: { action: action, carrier_id: carrierId, equipment_id: equipmentId, type_id: typeId, label: label, position_te: position, fk_protection: fkProtection, protection_label: protectionLabel, field_values: JSON.stringify(fieldValues), token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { self.isSaving = false; if (response.success) { $('#kundenkarte-equipment-dialog').remove(); // Reload page to get fresh data location.reload(); } else { $('#equipment-save').prop('disabled', false); KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; $('#equipment-save').prop('disabled', false); KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, duplicateEquipment: function(equipmentId) { var self = this; if (self.isSaving) return; self.isSaving = true; $.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) { self.isSaving = false; if (response.success) { location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { self.isSaving = false; KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, deleteEquipment: function(equipmentId) { var self = this; // No confirm here - handled by caller $.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) { location.reload(); } else { self.showAlertDialog('Fehler', response.error); } } }); }, moveEquipment: function(equipmentId, newPosition) { var self = this; $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', method: 'POST', data: { action: 'move', equipment_id: equipmentId, position_te: newPosition, token: $('input[name="token"]').val() }, dataType: 'json', success: function(response) { if (response.success) { location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } } }); }, showEquipmentTooltip: function($block, data) { var self = this; var html = '
'; html += '' + this.escapeHtml(data.type || '') + ''; if (data.label) { html += '
' + this.escapeHtml(data.label) + ''; } html += '
'; if (data.fields && Object.keys(data.fields).length > 0) { html += '
'; for (var key in data.fields) { if (data.fields[key]) { html += '
' + this.escapeHtml(key) + ': '; html += '' + this.escapeHtml(data.fields[key]) + '
'; } } html += '
'; } var $tooltip = $('#kundenkarte-equipment-tooltip'); if (!$tooltip.length) { $tooltip = $('
'); $('body').append($tooltip); } $tooltip.html(html); // Position near the block var offset = $block.offset ? $block.offset() : $(this).offset(); var rect = $block[0].getBoundingClientRect ? $block[0].getBoundingClientRect() : { right: 0, top: 0 }; $tooltip.css({ top: rect.top + window.scrollY + 10, left: rect.right + window.scrollX + 10 }).addClass('visible'); }, hideEquipmentTooltip: function() { $('#kundenkarte-equipment-tooltip').removeClass('visible'); }, // ========================================== // CONNECTION METHODS (Stromverbindungen) // ========================================== showOutputDialog: function(carrierId) { var self = this; if ($('#kundenkarte-output-dialog').length) return; // Get equipment on this carrier for dropdown var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); var equipmentOptions = []; $carrier.find('.kundenkarte-equipment-block').each(function() { var $block = $(this); var tooltipData = $block.data('tooltip'); var id = $block.data('equipment-id'); var label = tooltipData ? (tooltipData.label || tooltipData.type || 'Equipment ' + id) : 'Equipment ' + id; equipmentOptions.push({ id: id, label: label }); }); var html = '
'; html += '
'; html += '

Abgang hinzufügen

'; html += '×
'; html += '
'; html += '
'; // Equipment selection html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Phase selection html += '
'; html += '
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; html += '
'; // Consumer label html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Cable info html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
'; // form html += '
'; // body html += ''; html += '
'; $('body').append(html); $('#kundenkarte-output-dialog').addClass('visible'); // Phase button handlers $('#kundenkarte-output-dialog .kundenkarte-phase-btn').on('click', function() { $('#kundenkarte-output-dialog .kundenkarte-phase-btn').removeClass('active'); $(this).addClass('active'); $('input[name="output_connection_type"]').val($(this).data('phase')); }); // Save $('#output-save').on('click', function() { self.saveOutput(carrierId); }); // Close $('#output-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-output-dialog').remove(); }); }, saveOutput: function(carrierId) { var self = this; var data = { action: 'create_output', carrier_id: carrierId, equipment_id: $('select[name="output_equipment_id"]').val(), connection_type: $('input[name="output_connection_type"]').val(), output_label: $('input[name="output_consumer_label"]').val(), medium_type: $('select[name="output_cable_type"]').val(), medium_spec: $('select[name="output_cable_section"]').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) { $('#kundenkarte-output-dialog').remove(); location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, showRailDialog: function(carrierId) { var self = this; if ($('#kundenkarte-rail-dialog').length) return; // Get carrier info var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); var totalTE = 12; var infoText = $carrier.find('.kundenkarte-carrier-info').text(); var match = infoText.match(/\/(\d+)/); if (match) totalTE = parseInt(match[1]); var html = '
'; html += '
'; html += '

Sammelschiene hinzufügen

'; html += '×
'; html += '
'; html += '
'; // Quick presets row html += '
'; html += ''; html += '
'; // Electrical presets html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '|'; // Network presets html += ''; html += ''; html += ''; html += '
'; html += '
'; // Connection type (free text) html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Position (above/below) html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // TE range html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Multi-phase rail options html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Excluded positions (for FI switches etc.) html += '
'; html += '
'; html += ''; html += ''; html += 'An diesen Positionen wird die Schiene unterbrochen'; html += '
'; html += '
'; html += '
'; // form html += '
'; // body html += ''; html += '
'; $('body').append(html); $('#kundenkarte-rail-dialog').addClass('visible'); // Preset button handlers $('#kundenkarte-rail-dialog .rail-preset-btn').on('click', function() { var type = $(this).data('type'); $('input[name="rail_connection_type"]').val(type); $('input[name="rail_color"]').val($(this).data('color')); // Auto-set rail_phases for electrical presets var phasesSelect = $('select[name="rail_phases"]'); if (type === 'L1') phasesSelect.val('L1'); else if (type === 'L1N') phasesSelect.val('L1N'); else if (type === '3P') phasesSelect.val('3P'); else if (type === '3P+N') phasesSelect.val('3P+N'); else phasesSelect.val(''); // Simple line for N, PE, network cables }); // Save $('#rail-save').on('click', function() { self.saveRail(carrierId); }); // Close $('#rail-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-rail-dialog').remove(); }); }, saveRail: function(carrierId) { var self = this; // position_y: -1 = above equipment, 0+ = below equipment var positionY = $('select[name="rail_position"]').val() === 'above' ? -1 : 0; var data = { action: 'create_rail', carrier_id: carrierId, connection_type: $('input[name="rail_connection_type"]').val(), color: $('input[name="rail_color"]').val(), rail_start_te: $('input[name="rail_start_te"]').val(), rail_end_te: $('input[name="rail_end_te"]').val(), rail_phases: $('select[name="rail_phases"]').val(), excluded_te: $('input[name="rail_excluded_te"]').val(), position_y: positionY, 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) { $('#kundenkarte-rail-dialog').remove(); location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, showEditRailDialog: function(connectionId, carrierId) { var self = this; if ($('#kundenkarte-rail-dialog').length) return; // Load connection data first $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', data: { action: 'get', connection_id: connectionId }, dataType: 'json', success: function(response) { if (response.success && response.connection) { self.renderEditRailDialog(connectionId, carrierId, response.connection); } else { KundenKarte.showAlert('Fehler', 'Verbindung nicht gefunden'); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, renderEditRailDialog: function(connectionId, carrierId, conn) { var self = this; // Get carrier info var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); var totalTE = 12; var infoText = $carrier.find('.kundenkarte-carrier-info').text(); var match = infoText.match(/\/(\d+)/); if (match) totalTE = parseInt(match[1]); var isAbove = conn.position_y < 0; var html = '
'; html += '
'; html += '

Sammelschiene bearbeiten

'; html += '×
'; html += '
'; html += ''; html += '
'; // Quick presets row html += '
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '|'; html += ''; html += ''; html += ''; html += '
'; html += '
'; // Connection type (free text) html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Position (above/below) html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // TE range html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Multi-phase rail options html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Excluded positions (for FI switches etc.) html += '
'; html += '
'; html += ''; html += ''; html += 'An diesen Positionen wird die Schiene unterbrochen'; html += '
'; html += '
'; html += '
'; // form html += '
'; // body html += ''; html += '
'; $('body').append(html); $('#kundenkarte-rail-dialog').addClass('visible'); // Preset button handlers $('#kundenkarte-rail-dialog .rail-preset-btn').on('click', function() { var type = $(this).data('type'); $('input[name="rail_connection_type"]').val(type); $('input[name="rail_color"]').val($(this).data('color')); // Auto-set rail_phases for electrical presets var phasesSelect = $('select[name="rail_phases"]'); if (type === 'L1') phasesSelect.val('L1'); else if (type === 'L1N') phasesSelect.val('L1N'); else if (type === '3P') phasesSelect.val('3P'); else if (type === '3P+N') phasesSelect.val('3P+N'); else phasesSelect.val(''); // Simple line for N, PE, network cables }); // Save $('#rail-save').on('click', function() { self.updateRail(connectionId, carrierId); }); // Delete $('#rail-delete').on('click', function() { $('#kundenkarte-rail-dialog').remove(); self.showConfirmDialog('Sammelschiene löschen', 'Möchten Sie diese Sammelschiene wirklich löschen?', function() { self.doDeleteConnection(connectionId); }); }); // Close $('#rail-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-rail-dialog').remove(); }); }, updateRail: function(connectionId, carrierId) { var self = this; var positionY = $('select[name="rail_position"]').val() === 'above' ? -1 : 0; var data = { action: 'update', connection_id: connectionId, carrier_id: carrierId, connection_type: $('input[name="rail_connection_type"]').val(), color: $('input[name="rail_color"]').val(), rail_start_te: $('input[name="rail_start_te"]').val(), rail_end_te: $('input[name="rail_end_te"]').val(), rail_phases: $('select[name="rail_phases"]').val(), excluded_te: $('input[name="rail_excluded_te"]').val(), position_y: positionY, is_rail: 1, 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) { $('#kundenkarte-rail-dialog').remove(); location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, showEditOutputDialog: function(connectionId, carrierId) { var self = this; if ($('#kundenkarte-output-dialog').length) return; // Load connection data first $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', data: { action: 'get', connection_id: connectionId }, dataType: 'json', success: function(response) { if (response.success && response.connection) { self.renderEditOutputDialog(connectionId, carrierId, response.connection); } else { KundenKarte.showAlert('Fehler', 'Verbindung nicht gefunden'); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, renderEditOutputDialog: function(connectionId, carrierId, conn) { var self = this; var html = '
'; html += '
'; html += '

Abgang bearbeiten

'; html += '×
'; html += '
'; html += ''; html += ''; html += '
'; // Quick presets html += '
'; html += ''; html += '
'; html += ''; html += ''; html += ''; html += '|'; html += ''; html += ''; html += '
'; html += '
'; // Output label (consumer) html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Connection type and color html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Medium info html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; // Length html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
'; // form html += '
'; // body html += ''; html += '
'; $('body').append(html); $('#kundenkarte-output-dialog').addClass('visible'); // Preset button handlers $('#kundenkarte-output-dialog .output-preset-btn').on('click', function() { $('input[name="output_connection_type"]').val($(this).data('type')); $('input[name="output_color"]').val($(this).data('color')); }); // Save $('#output-save').on('click', function() { self.updateOutput(connectionId, carrierId); }); // Delete $('#output-delete').on('click', function() { $('#kundenkarte-output-dialog').remove(); self.showConfirmDialog('Abgang löschen', 'Möchten Sie diesen Abgang wirklich löschen?', function() { self.doDeleteConnection(connectionId); }); }); // Close $('#output-cancel, .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-output-dialog').remove(); }); }, updateOutput: function(connectionId, carrierId) { var self = this; var data = { action: 'update', connection_id: connectionId, carrier_id: carrierId, fk_source: $('input[name="output_fk_source"]').val(), source_terminal: 'output', connection_type: $('input[name="output_connection_type"]').val(), color: $('input[name="output_color"]').val(), output_label: $('input[name="output_label"]').val(), medium_type: $('input[name="output_medium_type"]').val(), medium_spec: $('input[name="output_medium_spec"]').val(), medium_length: $('input[name="output_medium_length"]').val(), is_rail: 0, 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) { $('#kundenkarte-output-dialog').remove(); location.reload(); } else { KundenKarte.showAlert('Fehler', response.error); } }, error: function() { KundenKarte.showAlert('Fehler', 'Netzwerkfehler'); } }); }, deleteConnection: function(connectionId) { var self = this; self.showConfirmDialog('Verbindung löschen', 'Möchten Sie diese Verbindung wirklich löschen?', function() { self.doDeleteConnection(connectionId); }); }, // Internal function to delete connection without confirmation doDeleteConnection: 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) { location.reload(); } else { self.showAlertDialog('Fehler', response.error); } } }); }, // Custom confirm dialog (replaces browser confirm()) showConfirmDialog: function(title, message, onConfirm, onCancel) { var self = this; // Remove any existing confirm dialog $('#kundenkarte-confirm-dialog').remove(); var html = '
'; html += '
'; html += '

' + self.escapeHtml(title) + '

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

' + self.escapeHtml(message) + '

'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-confirm-dialog').addClass('visible'); // Focus yes button $('#confirm-yes').focus(); // Yes button $('#confirm-yes').on('click', function() { $('#kundenkarte-confirm-dialog').remove(); if (typeof onConfirm === 'function') { onConfirm(); } }); // No button and close $('#confirm-no, #kundenkarte-confirm-dialog .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-confirm-dialog').remove(); if (typeof onCancel === 'function') { onCancel(); } }); // ESC key $(document).one('keydown.confirmDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-confirm-dialog').remove(); if (typeof onCancel === 'function') { onCancel(); } } else if (e.key === 'Enter') { $('#kundenkarte-confirm-dialog').remove(); if (typeof onConfirm === 'function') { onConfirm(); } } }); }, // Custom alert dialog (replaces browser alert()) showAlertDialog: function(title, message, onClose) { var self = this; // Remove any existing alert dialog $('#kundenkarte-alert-dialog').remove(); var html = '
'; html += '
'; html += '

' + self.escapeHtml(title) + '

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

' + self.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(); } }); }, escapeHtml: function(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, // BOM (Bill of Materials) / Stückliste Dialog showBOMDialog: function() { var self = this; var anlageId = this.anlageId; // Show loading var html = '
'; html += '
'; html += '
'; html += '

Stückliste (BOM)

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

Lade Stückliste...
'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-bom-dialog').addClass('visible'); // Close handler $('#bom-close, #kundenkarte-bom-dialog .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-bom-dialog').remove(); }); $(document).on('keydown.bomDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-bom-dialog').remove(); $(document).off('keydown.bomDialog'); } }); // Load BOM data $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/bom_generator.php', data: { action: 'generate', anlage_id: anlageId }, dataType: 'json', success: function(response) { if (response.success) { self.renderBOMContent(response); } else { $('.bom-loading').html('
' + (response.error || 'Fehler beim Laden') + '
'); } }, error: function() { $('.bom-loading').html('
AJAX Fehler
'); } }); }, renderBOMContent: function(data) { var self = this; var $body = $('#kundenkarte-bom-dialog .kundenkarte-modal-body'); if (!data.summary || data.summary.length === 0) { $body.html('


Keine Materialien im Schaltplan gefunden.
Fügen Sie Equipment hinzu oder verknüpfen Sie Produkte.
'); return; } var html = ''; // Summary table (grouped by product) html += '

Zusammenfassung nach Produkt

'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; var totalWithPrice = 0; var totalQuantity = 0; data.summary.forEach(function(item, index) { var rowClass = index % 2 === 0 ? 'oddeven' : 'oddeven'; var hasProduct = item.product_id ? true : false; var priceCell = hasProduct && item.price ? self.formatPrice(item.price) + ' €' : '-'; var totalCell = hasProduct && item.total ? self.formatPrice(item.total) + ' €' : '-'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; totalQuantity += item.quantity; if (hasProduct && item.total) { totalWithPrice += item.total; } }); // Totals row html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += '
ReferenzBezeichnungMengeEinzelpreisGesamt
' + self.escapeHtml(item.product_ref || '-') + '' + self.escapeHtml(item.product_label || '-'); if (!hasProduct) { html += ' (kein Produkt verknüpft)'; } html += '' + item.quantity + '' + priceCell + '' + totalCell + '
Summe' + totalQuantity + ' Stück' + self.formatPrice(totalWithPrice) + ' €
'; // Detailed list (collapsible) if (data.items && data.items.length > 0) { html += '
'; html += ' Detailliste (' + data.items.length + ' Einträge)'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; data.items.forEach(function(item, index) { var rowClass = index % 2 === 0 ? 'oddeven' : 'oddeven'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
EquipmentTypFeld/HutschieneBreiteProdukt
' + self.escapeHtml(item.equipment_label || '-') + '' + self.escapeHtml(item.type_ref || '') + ' ' + self.escapeHtml(item.type_label || '-') + '' + self.escapeHtml(item.panel_label || '-') + ' / ' + self.escapeHtml(item.carrier_label || '-') + '' + (item.width_te || 1) + ' TE' + (item.product_ref ? self.escapeHtml(item.product_ref) : '-') + '
'; html += '
'; } // Export buttons html += '
'; html += ''; html += '
'; $body.html(html); // Update totals in footer $('.bom-totals').html('' + totalQuantity + ' Artikel | ' + self.formatPrice(totalWithPrice) + ' € (geschätzt)'); // Copy to clipboard $body.find('.bom-copy-clipboard').on('click', function() { var text = 'Stückliste\n\n'; text += 'Referenz\tBezeichnung\tMenge\tEinzelpreis\tGesamt\n'; data.summary.forEach(function(item) { text += (item.product_ref || '-') + '\t'; text += (item.product_label || '-') + '\t'; text += item.quantity + '\t'; text += (item.price ? self.formatPrice(item.price) + ' €' : '-') + '\t'; text += (item.total ? self.formatPrice(item.total) + ' €' : '-') + '\n'; }); text += '\nSumme:\t\t' + totalQuantity + ' Stück\t\t' + self.formatPrice(totalWithPrice) + ' €'; navigator.clipboard.writeText(text).then(function() { KundenKarte.showNotification('In Zwischenablage kopiert', 'success'); }).catch(function() { KundenKarte.showError('Fehler', 'Kopieren nicht möglich'); }); }); }, formatPrice: function(price) { if (!price) return '0,00'; return parseFloat(price).toFixed(2).replace('.', ','); }, // Audit Log Dialog showAuditLogDialog: function() { var self = this; var anlageId = this.anlageId; var html = '
'; html += '
'; html += '
'; html += '

Änderungsprotokoll

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

Lade Protokoll...
'; html += '
'; html += ''; html += '
'; $('body').append(html); $('#kundenkarte-audit-dialog').addClass('visible'); // Close handler $('#audit-close, #kundenkarte-audit-dialog .kundenkarte-modal-close').on('click', function() { $('#kundenkarte-audit-dialog').remove(); }); $(document).on('keydown.auditDialog', function(e) { if (e.key === 'Escape') { $('#kundenkarte-audit-dialog').remove(); $(document).off('keydown.auditDialog'); } }); // Load audit data $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/audit_log.php', data: { action: 'fetch_anlage', anlage_id: anlageId, limit: 100 }, dataType: 'json', success: function(response) { if (response.success) { self.renderAuditContent(response.logs); } else { $('.audit-loading').html('
' + (response.error || 'Fehler beim Laden') + '
'); } }, error: function() { $('.audit-loading').html('
AJAX Fehler
'); } }); }, renderAuditContent: function(logs) { var self = this; var $body = $('#kundenkarte-audit-dialog .kundenkarte-modal-body'); if (!logs || logs.length === 0) { $body.html('


Keine Änderungen protokolliert.
'); return; } var html = '
'; logs.forEach(function(log) { html += '
'; html += '
'; html += ''; html += '
'; html += '
'; html += '
'; html += '' + self.escapeHtml(log.action_label) + ''; html += '' + self.escapeHtml(log.date_action) + ''; html += '
'; html += '
'; html += '' + self.escapeHtml(log.object_type_label) + ' '; html += self.escapeHtml(log.object_ref || 'ID ' + log.object_id); html += '
'; if (log.field_changed) { html += '
'; html += 'Feld: ' + self.escapeHtml(log.field_changed); if (log.old_value || log.new_value) { html += ' (' + self.escapeHtml(log.old_value || '-') + '' + self.escapeHtml(log.new_value || '-') + ')'; } html += '
'; } if (log.note) { html += '
' + self.escapeHtml(log.note) + '
'; } html += '
'; html += ' ' + self.escapeHtml(log.user_name || log.user_login); html += '
'; html += '
'; }); html += '
'; $body.html(html); } }; /** * Connection Editor Component * Interactive SVG-based connection editor with orthogonal routing * Supports busbars, multi-phase connections, and drag-drop */ KundenKarte.ConnectionEditor = { TE_WIDTH: 50, BLOCK_HEIGHT: 110, BUSBAR_HEIGHT: 25, BUSBAR_SPACING: 8, CONNECTION_AREA_HEIGHT: 120, ENDPOINT_RADIUS: 6, // Phase colors (German electrical standard) PHASE_COLORS: { 'L1': '#8B4513', // Brown 'L2': '#000000', // Black 'L3': '#808080', // Gray 'N': '#0066cc', // Blue 'PE': '#27ae60', // Green-Yellow 'LN': '#8B4513', // Single phase output (brown - phase determined by busbar) 'L1N': '#3498db', // L1 + N input (legacy/busbar) '3P': '#2c3e50', // 3-phase '3P+N': '#34495e', // 3-phase + neutral 'DATA': '#9b59b6' // Data cable (purple) }, // State isExpanded: false, currentCarrierId: null, currentAnlageId: null, connections: [], busbars: [], equipment: [], dragState: null, selectedConnection: null, DEBUG: false, // Set to true to enable debug logging // Conditional logging log: function() { if (this.DEBUG && console && console.log) { console.log.apply(console, arguments); } }, init: function(anlageId) { this.log('ConnectionEditor.init called with anlageId:', anlageId); if (!anlageId) { this.log('ConnectionEditor: No anlageId, aborting'); return; } this.currentAnlageId = anlageId; // Restore expanded state from localStorage var savedState = localStorage.getItem('kundenkarte_connection_editor_expanded'); this.isExpanded = savedState === 'true'; this.log('ConnectionEditor: isExpanded =', this.isExpanded); this.bindEvents(); this.log('ConnectionEditor: Events bound'); this.renderAllEditors(); this.log('ConnectionEditor: Editors rendered'); }, bindEvents: function() { var self = this; // Toggle editor visibility $(document).on('click', '.kundenkarte-connection-editor-toggle', function(e) { e.preventDefault(); var $editor = $(this).closest('.kundenkarte-carrier').find('.kundenkarte-connection-editor'); var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); $editor.toggleClass('expanded'); $(this).find('i').toggleClass('fa-chevron-down fa-chevron-up'); // Save state self.isExpanded = $editor.hasClass('expanded'); localStorage.setItem('kundenkarte_connection_editor_expanded', self.isExpanded); if (self.isExpanded) { self.loadAndRenderEditor(carrierId); } }); // Add busbar button $(document).on('click', '.kundenkarte-add-busbar-btn', function(e) { e.preventDefault(); var carrierId = $(this).data('carrier-id'); self.showBusbarDialog(carrierId); }); // Add connection button $(document).on('click', '.kundenkarte-add-conn-btn', function(e) { e.preventDefault(); var carrierId = $(this).data('carrier-id'); self.showConnectionDialog(carrierId); }); // SVG mouse events for drag-drop connections $(document).on('mousedown', '.kundenkarte-endpoint', function(e) { e.preventDefault(); e.stopPropagation(); var $endpoint = $(this); var carrierId = $endpoint.closest('.kundenkarte-connection-editor').data('carrier-id'); self.startDragConnection(e, $endpoint, carrierId); }); $(document).on('mousemove', function(e) { if (self.dragState) { self.updateDragConnection(e); } }); $(document).on('mouseup', function(e) { if (self.dragState) { self.endDragConnection(e); } }); // Click on busbar to edit $(document).on('click', '.kundenkarte-busbar-element', function(e) { e.preventDefault(); e.stopPropagation(); var connectionId = $(this).data('connection-id'); var carrierId = $(this).closest('.kundenkarte-connection-editor').data('carrier-id'); self.showEditBusbarDialog(connectionId, carrierId); }); // Click on connection line to edit/delete $(document).on('click', '.kundenkarte-connection-path', function(e) { e.preventDefault(); e.stopPropagation(); var connectionId = $(this).data('connection-id'); var carrierId = $(this).closest('.kundenkarte-connection-editor').data('carrier-id'); self.showEditConnectionDialog(connectionId, carrierId); }); // Hover effects $(document).on('mouseenter', '.kundenkarte-endpoint', function() { $(this).addClass('hover'); }); $(document).on('mouseleave', '.kundenkarte-endpoint', function() { $(this).removeClass('hover'); }); }, renderAllEditors: function() { var self = this; $('.kundenkarte-carrier').each(function() { var carrierId = $(this).data('carrier-id'); var $editor = $(this).find('.kundenkarte-connection-editor'); // Editor is already rendered via PHP, just need to initialize state if ($editor.length) { if (self.isExpanded) { $editor.addClass('expanded'); $(this).find('.kundenkarte-connection-editor-toggle i').removeClass('fa-chevron-down').addClass('fa-chevron-up'); self.loadAndRenderEditor(carrierId); } } }); }, loadAndRenderEditor: function(carrierId) { var self = this; // Load equipment for this carrier $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', data: { action: 'list', carrier_id: carrierId }, dataType: 'json', success: function(response) { if (response.success) { self.equipment = response.equipment || []; // Load connections $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', data: { action: 'list', carrier_id: carrierId }, dataType: 'json', success: function(connResponse) { if (connResponse.success) { self.connections = connResponse.connections || []; self.busbars = self.connections.filter(function(c) { return c.is_rail == 1; }); self.renderEditor(carrierId); } } }); } } }); }, renderEditor: function(carrierId) { var self = this; var $editor = $('.kundenkarte-connection-editor[data-carrier-id="' + carrierId + '"]'); var $svg = $editor.find('.kundenkarte-connection-svg'); var totalTE = parseInt($editor.data('total-te')) || 12; var totalWidth = totalTE * this.TE_WIDTH; // Reset busbar positions this.busbarPositions = {}; var svgContent = ''; // Defs for markers and gradients svgContent += ''; svgContent += ''; svgContent += ''; svgContent += ''; // Phase gradients Object.keys(this.PHASE_COLORS).forEach(function(phase) { svgContent += ''; svgContent += ''; svgContent += ''; svgContent += ''; }); svgContent += ''; // If no busbars exist, show a demo if (this.busbars.length === 0 && this.equipment.length > 0) { svgContent += this.renderDemoView(carrierId, totalTE, totalWidth); $svg.html(svgContent); $svg.attr('height', 200); return; } // Background grid for (var i = 0; i <= totalTE; i++) { var x = i * this.TE_WIDTH; svgContent += ''; } // Render busbars var busbarY = 15; var busbarsByPosition = {}; this.busbars.forEach(function(busbar, index) { var posKey = busbar.position_y < 0 ? 'above' : busbar.position_y; if (!busbarsByPosition[posKey]) busbarsByPosition[posKey] = []; busbarsByPosition[posKey].push(busbar); }); Object.keys(busbarsByPosition).forEach(function(posKey) { busbarsByPosition[posKey].forEach(function(busbar, idx) { svgContent += self.renderBusbar(busbar, carrierId, busbarY + (idx * (self.BUSBAR_HEIGHT + self.BUSBAR_SPACING))); }); busbarY += busbarsByPosition[posKey].length * (self.BUSBAR_HEIGHT + self.BUSBAR_SPACING); }); // Render equipment endpoints var equipmentY = busbarY + 20; // Store equipment positions for connection routing this.equipmentPositions = {}; this.equipment.forEach(function(eq) { var width = eq.width_te * self.TE_WIDTH; var centerX = (eq.position_te - 1) * self.TE_WIDTH + width / 2; self.equipmentPositions[eq.id] = { x: centerX, left: (eq.position_te - 1) * self.TE_WIDTH, right: eq.position_te * self.TE_WIDTH + (eq.width_te - 1) * self.TE_WIDTH, inputY: equipmentY - 15, outputY: equipmentY + 15, centerY: equipmentY }; }); // Render non-busbar connections FIRST (behind equipment) // But route them around blocks, not through them var regularConnections = this.connections.filter(function(c) { return c.is_rail != 1; }); // Connection layer - rendered below equipment layer svgContent += ''; regularConnections.forEach(function(conn, index) { svgContent += self.renderOrthogonalConnection(conn, carrierId, equipmentY, index); }); svgContent += ''; // Equipment layer - rendered on top svgContent += ''; this.equipment.forEach(function(eq) { svgContent += self.renderEquipmentEndpoints(eq, equipmentY, carrierId); }); svgContent += ''; // Render drag preview line (hidden by default) - on top of everything svgContent += '