';
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 += '';
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 += '
';
// 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 += '
';
// Upload area
html += '
';
html += '';
html += '
Icon hochladen (PNG, JPG, SVG, max 500KB)
';
html += '';
html += '';
html += '
';
// Custom icons grid
html += '
';
html += '
Lade eigene Icons...
';
html += '
';
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 = '
' + 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 += '';
// 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 = '';
// Block rectangle with rounded corners
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 = '
';
}
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 += '
');
}
});
},
loadEquipment: function() {
var self = this;
var promises = [];
this.equipment = [];
this.carriers.forEach(function(carrier) {
var promise = $.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
data: { action: 'list', carrier_id: carrier.id },
dataType: 'json'
}).then(function(response) {
if (response.success && response.equipment) {
response.equipment.forEach(function(eq) {
eq.carrier_id = carrier.id;
eq.panel_id = carrier.panel_id;
self.equipment.push(eq);
});
}
});
promises.push(promise);
});
$.when.apply($, promises).then(function() {
self.loadConnections();
}).fail(function() {
self.isLoading = false;
});
},
loadConnections: function() {
var self = this;
// Load connections and bridges in parallel
var connectionsLoaded = false;
var bridgesLoaded = false;
var checkComplete = function() {
if (connectionsLoaded && bridgesLoaded) {
// Initialize canvas now that all data is loaded
if (!self.isInitialized) {
self.initCanvas();
} else {
self.render();
}
// Reset loading flag
self.isLoading = false;
}
};
// Load connections
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php',
data: { action: 'list_all', anlage_id: this.anlageId },
dataType: 'json',
success: function(response) {
if (response.success) {
self.connections = response.connections || [];
// Restore any manually drawn paths
if (self._manualPaths) {
self.connections.forEach(function(conn) {
if (self._manualPaths[conn.id]) {
conn._manualPath = self._manualPaths[conn.id];
}
});
}
}
connectionsLoaded = true;
checkComplete();
},
error: function() {
connectionsLoaded = true;
checkComplete();
}
});
// Load bridges
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php',
data: { action: 'list_bridges', anlage_id: this.anlageId },
dataType: 'json',
success: function(response) {
if (response.success) {
self.bridges = response.bridges || [];
}
bridgesLoaded = true;
checkComplete();
},
error: function() {
bridgesLoaded = true;
checkComplete();
}
});
},
calculateLayout: function() {
var self = this;
// Calculate dynamic TOP_MARGIN based on number of connections going up
// Count connections that route above blocks (top-to-top connections)
var topConnections = 0;
this.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) !== 1 && conn.fk_source && conn.fk_target) {
topConnections++;
}
});
// Panel top margin (where the panel border starts)
this.panelTopMargin = this.MIN_TOP_MARGIN;
// Block top margin = panel margin + inner offset + connection routing space
this.calculatedTopMargin = this.panelTopMargin + this.BLOCK_INNER_OFFSET + topConnections * this.CONNECTION_ROUTE_SPACE;
// Calculate canvas size based on panels
// Panels nebeneinander, Hutschienen pro Panel untereinander
var totalWidth = this.PANEL_PADDING;
var maxPanelHeight = 0;
this.panels.forEach(function(panel) {
var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; });
var maxTE = 12;
panelCarriers.forEach(function(c) {
if ((c.total_te || 12) > maxTE) maxTE = c.total_te;
});
// Panel width includes: left margin for label (60px) + rail overhang (30px) + TE width + rail overhang (30px) + padding
var panelWidth = 60 + 30 + maxTE * self.TE_WIDTH + 30 + self.PANEL_PADDING;
// Panel height calculation:
// - calculatedTopMargin: space for main busbars at top
// - BLOCK_HEIGHT/2: half block above first rail
// - (carriers-1) * RAIL_SPACING: spacing between rails
// - BLOCK_HEIGHT/2: half block below last rail
// - BOTTOM_MARGIN: margin at bottom
// Simplified: calculatedTopMargin + BLOCK_HEIGHT + (carriers * RAIL_SPACING) + BOTTOM_MARGIN
var carrierCount = Math.max(panelCarriers.length, 1);
var panelHeight = self.calculatedTopMargin + self.BLOCK_HEIGHT + (carrierCount * self.RAIL_SPACING) + self.BOTTOM_MARGIN;
panel._x = totalWidth;
panel._width = panelWidth;
panel._height = panelHeight;
totalWidth += panelWidth + self.PANEL_GAP;
if (panelHeight > maxPanelHeight) maxPanelHeight = panelHeight;
});
totalWidth = Math.max(totalWidth, 400);
var totalHeight = Math.max(maxPanelHeight, 300);
// Fallback: wenn keine Panels, basierend auf Carriers
if (this.panels.length === 0 && this.carriers.length > 0) {
var fallbackMaxTE = 12;
this.carriers.forEach(function(c) {
if ((c.total_te || 12) > fallbackMaxTE) fallbackMaxTE = c.total_te;
});
totalWidth = fallbackMaxTE * self.TE_WIDTH + 150;
totalHeight = self.calculatedTopMargin + self.BLOCK_HEIGHT + (this.carriers.length * self.RAIL_SPACING) + self.BOTTOM_MARGIN;
}
// SVG mindestens so breit wie der Container (ohne zu skalieren)
var $canvas = $('.schematic-editor-canvas');
var containerWidth = $canvas.length ? ($canvas.innerWidth() - 30) : 800;
this.layoutWidth = Math.max(totalWidth, containerWidth, 800);
this.layoutHeight = Math.max(totalHeight, 400);
},
initCanvas: function() {
var $canvas = $('.schematic-editor-canvas');
if (!$canvas.length) {
return;
}
// Loading message
$canvas.html('
Initialisiere Schaltplan-Editor...
');
// Calculate layout first
this.calculateLayout();
// Create SVG - flexible width with viewBox for content scaling
// data-darkreader-mode="disabled" tells Dark Reader to skip this element
var svg = '';
// Wrap SVG in zoom wrapper for coordinated scaling with controls
$canvas.html('
' + svg + '
');
this.svgElement = $canvas.find('.schematic-svg')[0];
this.isInitialized = true;
// Render content
this.render();
},
render: function() {
if (!this.isInitialized) return;
// Recalculate layout (panels may have changed)
this.calculateLayout();
// Update SVG size
var $svg = $(this.svgElement);
$svg.attr('width', this.layoutWidth);
$svg.attr('height', this.layoutHeight);
this.renderRails();
this.renderBlocks();
this.renderBridges();
this.renderBusbars();
this.renderConnections();
this.renderControls();
// Adjust SVG height to fit all content
this.adjustSvgHeight();
},
// Dynamically adjust SVG height based on actual content
adjustSvgHeight: function() {
var $svg = $(this.svgElement);
if (!$svg.length) return;
// Find the lowest Y position of all rendered elements
var maxY = this.layoutHeight;
// Check all busbar positions
var self = this;
this.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) === 1) {
var carrier = self.carriers.find(function(c) { return String(c.id) === String(conn.fk_carrier); });
if (carrier && carrier._y !== undefined) {
var posY = parseInt(conn.position_y) || 0;
var railCenterY = carrier._y + self.RAIL_HEIGHT / 2;
var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2;
var busbarY = posY === 0 ? railCenterY - self.BLOCK_HEIGHT / 2 - 60 : blockBottom + 60;
if (busbarY + 40 > maxY) maxY = busbarY + 40;
}
}
});
// Check output/input connections (Abgänge)
$svg.find('.schematic-output-label, .schematic-input-label').each(function() {
var y = parseFloat($(this).attr('y')) || 0;
if (y + 30 > maxY) maxY = y + 30;
});
// Add padding
maxY += 40;
// Update SVG height if needed
if (maxY > this.layoutHeight) {
this.layoutHeight = maxY;
$svg.attr('height', maxY);
// Also update panel backgrounds
this.panels.forEach(function(panel) {
if (panel._height < maxY - self.panelTopMargin) {
panel._height = maxY;
}
});
}
},
renderRails: function() {
var self = this;
var $layer = $(this.svgElement).find('.schematic-rails-layer');
$layer.empty();
var html = '';
// Render by panels (nebeneinander)
if (this.panels.length > 0) {
this.panels.forEach(function(panel) {
var panelX = panel._x || self.PANEL_PADDING;
var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; });
// Panel background - starts at panel top margin (before block offset)
if (panel._width && panel._height) {
var panelTop = self.panelTopMargin;
var panelContentHeight = panel._height - self.panelTopMargin;
html += '';
// Panel label - klickbar für Bearbeitung
html += '';
html += self.escapeHtml(panel.label || 'Feld');
html += '';
}
// Carriers untereinander im Panel
// Layout: [LEFT_MARGIN for label+overhang] [TE slots] [RIGHT overhang+padding]
panelCarriers.forEach(function(carrier, carrierIdx) {
// Hutschiene is centered, blocks are centered on the Hutschiene
// Calculate rail position: TOP_MARGIN + half block height + carrier spacing
var railY = self.calculatedTopMargin + (self.BLOCK_HEIGHT / 2) + carrierIdx * self.RAIL_SPACING;
var width = (carrier.total_te || 12) * self.TE_WIDTH;
// x position: panelX + left margin (60px for label) + overhang (30px)
var x = panelX + 60 + 30;
// Store carrier position (rail Y position)
carrier._y = railY;
carrier._x = x;
// Rail overhang on both sides for label visibility
var railOverhang = 30;
// Rail background - Hutschiene (mit Überstand links und rechts)
html += '';
// Rail (mit Überstand links und rechts) - klickbar für Bearbeitung
html += '';
// Rail label (links vom Überstand, centered with rail)
html += '';
html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1));
html += '';
// TE-Markierungen auf der Schiene (ganzzahlige TEs deutlich, 0.5er-Zwischenmarken subtil)
var totalTECount = carrier.total_te || 12;
for (var te = 0; te <= totalTECount; te++) {
var teX = x + te * self.TE_WIDTH;
html += '';
// 0.5-TE Zwischenmarkierung
if (te < totalTECount) {
var halfX = teX + self.TE_WIDTH / 2;
html += '';
}
}
});
});
} else {
// Fallback: Carriers ohne Panels
this.carriers.forEach(function(carrier, idx) {
var blockTop = self.calculatedTopMargin + idx * self.RAIL_SPACING;
var railY = blockTop + self.BLOCK_HEIGHT + 10;
var width = (carrier.total_te || 12) * self.TE_WIDTH;
var x = self.PANEL_PADDING + 50;
carrier._y = railY;
carrier._x = x;
carrier._blockTop = blockTop;
// Rail overhang on both sides
var railOverhang = 30;
html += '';
html += '';
html += '';
html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1));
html += '';
// TE-Markierungen (ganzzahlige TEs deutlich, 0.5er subtil)
var totalTECount = carrier.total_te || 12;
for (var te = 0; te <= totalTECount; te++) {
var teX = x + te * self.TE_WIDTH;
html += '';
if (te < totalTECount) {
var halfX = teX + self.TE_WIDTH / 2;
html += '';
}
}
});
}
$layer.html(html);
},
renderBlocks: function() {
var self = this;
var $layer = $(this.svgElement).find('.schematic-blocks-layer');
var $terminalLayer = $(this.svgElement).find('.schematic-terminals-layer');
$layer.empty();
$terminalLayer.empty();
var blockHtml = '';
var terminalHtml = '';
this.equipment.forEach(function(eq) {
var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); });
if (!carrier) {
return;
}
if (typeof carrier._x === 'undefined' || carrier._x === null) {
console.log(' Equipment #' + eq.id + ' (' + eq.label + '): Carrier has no _x position set, skipping');
return;
}
var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4;
var blockHeight = self.BLOCK_HEIGHT;
var x = parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * self.TE_WIDTH + 2;
// Position blocks centered on the rail (Hutschiene)
// Rail is at carrier._y, block center should align with rail center
var railCenterY = carrier._y + self.RAIL_HEIGHT / 2;
var y = railCenterY - blockHeight / 2;
// Store position
eq._x = x;
eq._y = y;
eq._width = blockWidth;
eq._height = blockHeight;
var color = eq.block_color || eq.type_color || self.COLORS.block;
// Block group
blockHtml += '';
// Check if we have a block image
var hasBlockImage = eq.type_block_image && eq.type_block_image_url;
if (hasBlockImage) {
// Render image instead of colored rectangle
// Use xMidYMid slice to fill the block and crop if needed
blockHtml += '';
// Border around the image block
blockHtml += '';
} else {
// Block background with gradient (original behavior)
blockHtml += '';
// Build block text lines
var lines = [];
// Line 1: Type label (e.g., LS1P, FI4P)
lines.push({
text: self.escapeHtml(eq.type_label_short || eq.type_ref || ''),
fontSize: 13,
fontWeight: 'bold',
color: '#fff'
});
// Line 2: show_on_block fields (e.g., B16, 40A 30mA)
var blockFieldValues = [];
if (eq.type_fields && eq.field_values) {
eq.type_fields.forEach(function(field) {
if (field.show_on_block && eq.field_values[field.field_code]) {
var val = eq.field_values[field.field_code];
// Add unit for known fields
if (field.field_code === 'ampere') {
val = val + 'A';
} else if (field.field_code === 'sensitivity') {
val = val + 'mA';
}
blockFieldValues.push(val);
}
});
}
if (blockFieldValues.length > 0) {
lines.push({
text: blockFieldValues.join(' '),
fontSize: 11,
fontWeight: 'bold',
color: '#fff'
});
}
// Line 3: Custom label (e.g., R2.1 or "Licht Küche")
if (eq.label) {
lines.push({
text: self.escapeHtml(eq.label),
fontSize: 10,
fontWeight: 'normal',
color: 'rgba(255,255,255,0.8)'
});
}
// Calculate vertical positioning for centered text
var totalLineHeight = 0;
lines.forEach(function(line) {
totalLineHeight += line.fontSize + 2; // fontSize + 2px spacing
});
var startY = (blockHeight - totalLineHeight) / 2 + lines[0].fontSize;
// Render text lines
var currentY = startY;
lines.forEach(function(line) {
blockHtml += '';
blockHtml += line.text;
blockHtml += '';
currentY += line.fontSize + 3;
});
// Richtungspfeile (flow_direction)
var flowDir = eq.type_flow_direction;
if (flowDir) {
var arrowX = blockWidth - 12;
var arrowY1, arrowY2, arrowY3;
if (flowDir === 'top_to_bottom') {
// Pfeil nach unten ↓
arrowY1 = 10;
arrowY2 = blockHeight - 10;
blockHtml += '';
blockHtml += '';
} else if (flowDir === 'bottom_to_top') {
// Pfeil nach oben ↑
arrowY1 = blockHeight - 10;
arrowY2 = 10;
blockHtml += '';
blockHtml += '';
}
}
}
blockHtml += '';
// Terminals (bidirectional)
// WICHTIG: Terminals werden im festen TE-Raster platziert
// Jeder Terminal nimmt 1 TE ein - ein 3TE Block mit 3 Terminals
// sieht genauso aus wie 3 einzelne 1TE Blöcke
var terminals = self.getTerminals(eq);
var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; });
var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; });
var widthTE = parseFloat(eq.width_te) || 1;
// Check if this equipment is covered by a busbar (returns { top: bool, bottom: bool })
var busbarCoverage = self.isEquipmentCoveredByBusbar(eq);
// Terminal-Position aus Equipment-Typ (both, top_only, bottom_only)
var terminalPos = eq.type_terminal_position || 'both';
var showTopTerminals = (terminalPos === 'both' || terminalPos === 'top_only') && !busbarCoverage.top;
var showBottomTerminals = (terminalPos === 'both' || terminalPos === 'bottom_only') && !busbarCoverage.bottom;
// Top terminals - im TE-Raster platziert (hide if busbar covers this equipment or terminal_position)
// Now supports stacked terminals with 'row' property (for Reihenklemmen)
var terminalColor = '#666'; // Grau wie die Hutschienen
var terminalSpacing = 14; // Vertical spacing between stacked terminals
if (showTopTerminals) {
// Group terminals by column (teIndex) for stacking
var topTerminalsByCol = {};
topTerminals.forEach(function(term, idx) {
var teIndex = term.col !== undefined ? term.col : (idx % widthTE);
if (!topTerminalsByCol[teIndex]) topTerminalsByCol[teIndex] = [];
topTerminalsByCol[teIndex].push(term);
});
Object.keys(topTerminalsByCol).forEach(function(colKey) {
var colTerminals = topTerminalsByCol[colKey];
var teIndex = parseInt(colKey);
var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2);
colTerminals.forEach(function(term, rowIdx) {
var row = term.row !== undefined ? term.row : rowIdx;
var ty = y - 7 - (row * terminalSpacing);
terminalHtml += '';
terminalHtml += '';
// Label - position depends on whether there are stacked terminals
var labelText = self.escapeHtml(term.label);
var labelWidth = labelText.length * 6 + 6;
if (colTerminals.length > 1) {
// Stacked: place label to the left of the terminal
terminalHtml += '';
terminalHtml += '' + labelText + '';
} else {
// Single: place label below in block
terminalHtml += '';
terminalHtml += '' + labelText + '';
}
terminalHtml += '';
});
});
}
// Bottom terminals - im TE-Raster platziert (hide if busbar covers bottom)
// Now supports stacked terminals with 'row' property
if (showBottomTerminals) {
// Group terminals by column (teIndex) for stacking
var bottomTerminalsByCol = {};
bottomTerminals.forEach(function(term, idx) {
var teIndex = term.col !== undefined ? term.col : (idx % widthTE);
if (!bottomTerminalsByCol[teIndex]) bottomTerminalsByCol[teIndex] = [];
bottomTerminalsByCol[teIndex].push(term);
});
Object.keys(bottomTerminalsByCol).forEach(function(colKey) {
var colTerminals = bottomTerminalsByCol[colKey];
var teIndex = parseInt(colKey);
var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2);
colTerminals.forEach(function(term, rowIdx) {
var row = term.row !== undefined ? term.row : rowIdx;
var ty = y + blockHeight + 7 + (row * terminalSpacing);
terminalHtml += '';
terminalHtml += '';
// Label - position depends on whether there are stacked terminals
var labelText = self.escapeHtml(term.label);
var labelWidth = labelText.length * 6 + 6;
if (colTerminals.length > 1) {
// Stacked: place label to the right of the terminal
terminalHtml += '';
terminalHtml += '' + labelText + '';
} else {
// Single: place label above in block
terminalHtml += '';
terminalHtml += '' + labelText + '';
}
terminalHtml += '';
});
});
}
});
$layer.html(blockHtml);
$terminalLayer.html(terminalHtml);
},
renderBridges: function() {
// Render terminal bridges (Brücken zwischen Reihenklemmen)
var self = this;
// Create or get bridge layer (between terminals and busbars)
var $bridgeLayer = $(this.svgElement).find('.schematic-bridges-layer');
if ($bridgeLayer.length === 0) {
// Insert after terminals layer
var $terminalLayer = $(this.svgElement).find('.schematic-terminals-layer');
$terminalLayer.after('');
$bridgeLayer = $(this.svgElement).find('.schematic-bridges-layer');
}
$bridgeLayer.empty();
if (!this.bridges || this.bridges.length === 0) {
return;
}
var html = '';
this.bridges.forEach(function(bridge) {
// Find carrier for this bridge
var carrier = self.carriers.find(function(c) {
return String(c.id) === String(bridge.fk_carrier);
});
if (!carrier || typeof carrier._x === 'undefined') {
return;
}
// Calculate bridge positions
var startTE = parseInt(bridge.start_te) || 1;
var endTE = parseInt(bridge.end_te) || startTE;
var terminalSide = bridge.terminal_side || 'top';
var terminalRow = parseInt(bridge.terminal_row) || 0;
var color = bridge.color || '#e74c3c';
// X positions for bridge endpoints (center of each TE)
var x1 = carrier._x + (startTE - 1) * self.TE_WIDTH + self.TE_WIDTH / 2;
var x2 = carrier._x + (endTE - 1) * self.TE_WIDTH + self.TE_WIDTH / 2;
// Y position based on carrier and terminal side
var railY = carrier._y;
var blockTop = railY + self.RAIL_HEIGHT / 2 - self.BLOCK_HEIGHT / 2;
var blockBottom = blockTop + self.BLOCK_HEIGHT;
var terminalSpacing = 14;
var y;
if (terminalSide === 'top') {
// Bridge above block, at terminal level
y = blockTop - 7 - (terminalRow * terminalSpacing);
} else {
// Bridge below block, at terminal level
y = blockBottom + 7 + (terminalRow * terminalSpacing);
}
// Bridge is a horizontal bar connecting terminals
var bridgeHeight = 6;
html += '';
// Bridge bar (horizontal rectangle)
html += '';
// Connection points at each end
html += '';
html += '';
// Optional label
if (bridge.label) {
var labelX = (x1 + x2) / 2;
html += '';
html += self.escapeHtml(bridge.label);
html += '';
}
html += '';
});
$bridgeLayer.html(html);
},
renderBusbars: function() {
// Render Sammelschienen (busbars) - connections with is_rail=1
var self = this;
var $layer = $(this.svgElement).find('.schematic-busbars-layer');
$layer.empty();
var html = '';
var renderedCount = 0;
// First, group busbars by carrier and position for stacking
var busbarsByCarrierAndPos = {};
this.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) !== 1) return;
var key = conn.fk_carrier + '_' + (parseInt(conn.position_y) || 0);
if (!busbarsByCarrierAndPos[key]) busbarsByCarrierAndPos[key] = [];
busbarsByCarrierAndPos[key].push(conn);
});
this.connections.forEach(function(conn) {
// Only process rails/busbars
if (parseInt(conn.is_rail) !== 1) {
return;
}
// Get carrier for this busbar
var carrier = self.carriers.find(function(c) {
return String(c.id) === String(conn.fk_carrier);
});
if (!carrier || typeof carrier._x === 'undefined') {
console.log(' Busbar #' + conn.id + ': No carrier found or carrier has no position');
return;
}
// Busbar spans from rail_start_te to rail_end_te
var startTE = parseInt(conn.rail_start_te) || 1;
var endTE = parseInt(conn.rail_end_te) || startTE + 1;
// Calculate pixel positions
var startX = carrier._x + (startTE - 1) * self.TE_WIDTH;
var endX = carrier._x + endTE * self.TE_WIDTH;
var width = endX - startX;
// Position busbar above or below the blocks based on position_y
// position_y: 0 = above (top), 1 = below (bottom)
var posY = parseInt(conn.position_y) || 0;
var railCenterY = carrier._y + self.RAIL_HEIGHT / 2;
var blockTop = railCenterY - self.BLOCK_HEIGHT / 2;
var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2;
// Count how many busbars are already at this position (to stack them)
var key = conn.fk_carrier + '_' + posY;
var carrierBusbars = busbarsByCarrierAndPos[key] || [];
var busbarIndex = carrierBusbars.indexOf(conn);
if (busbarIndex < 0) busbarIndex = 0;
// Busbar height and position - stack multiple busbars
var busbarHeight = 24;
var busbarSpacing = busbarHeight + 20; // Space between stacked busbars (including label space)
var busbarY;
if (posY === 0) {
// Above blocks - stack upwards
busbarY = blockTop - busbarHeight - 25 - (busbarIndex * busbarSpacing);
} else {
// Below blocks - stack downwards
busbarY = blockBottom + 25 + (busbarIndex * busbarSpacing);
}
// Color from connection or default phase color
var color = conn.color || self.PHASE_COLORS[conn.connection_type] || '#e74c3c';
// Draw busbar as rounded rectangle
html += '';
// Shadow
html += '';
// Main bar
html += '';
// Parse excluded TEs
var excludedTEs = [];
if (conn.excluded_te) {
excludedTEs = conn.excluded_te.split(',').map(function(t) { return parseInt(t.trim()); }).filter(function(t) { return !isNaN(t); });
}
// Determine phase labels based on rail_phases
var phases = conn.rail_phases || conn.connection_type || '';
var phaseLabels = [];
if (phases === 'L1L2L3') {
phaseLabels = ['L1', 'L2', 'L3'];
} else if (phases === 'L1') {
phaseLabels = ['L1'];
} else if (phases === 'L2') {
phaseLabels = ['L2'];
} else if (phases === 'L3') {
phaseLabels = ['L3'];
} else if (phases === 'N') {
phaseLabels = ['N'];
} else if (phases === 'PE') {
phaseLabels = ['PE'];
} else if (phases) {
phaseLabels = [phases];
}
// Draw taps per TE (not per block) - each TE gets its own connection point
// Phase index counts ALL TEs from start (including excluded ones) for correct phase assignment
for (var te = startTE; te <= endTE; te++) {
// Calculate phase for this TE position (based on position from start, not rendered count)
var teOffset = te - startTE; // 0-based offset from start
var currentPhase = phaseLabels.length > 0 ? phaseLabels[teOffset % phaseLabels.length] : '';
// Skip excluded TEs (but phase still counts)
if (excludedTEs.indexOf(te) !== -1) continue;
// Calculate X position for this TE - center of the TE slot
// TE 1 starts at carrier._x, so center of TE 1 is at carrier._x + TE_WIDTH/2
// TE n center is at carrier._x + (n-1) * TE_WIDTH + TE_WIDTH/2
var teX = carrier._x + (te - 1) * self.TE_WIDTH + self.TE_WIDTH / 2;
// Draw tap line from busbar to block/terminal area
var tapStartY = posY === 0 ? busbarY + busbarHeight : busbarY;
var tapEndY;
if (posY === 0) {
tapEndY = blockTop - 7;
} else {
tapEndY = blockBottom + 7;
}
html += '';
// Connector dot at equipment end - same size as block terminals
html += '';
// Phase label at this TE connection
if (currentPhase) {
// Position label on the busbar
var phaseLabelY = busbarY + busbarHeight / 2 + 4;
html += '';
html += self.escapeHtml(currentPhase);
html += '';
}
}
// Main phase label in the CENTER of the busbar
if (phases) {
var labelX = startX + width / 2;
var labelY = busbarY + busbarHeight / 2 + 4;
html += '';
html += self.escapeHtml(phases);
html += '';
}
html += '';
renderedCount++;
console.log(' Busbar #' + conn.id + ': Rendered from TE ' + startTE + ' to ' + endTE + ' on carrier ' + carrier.id +
', x=' + startX + ', y=' + busbarY + ', width=' + width + ', color=' + color);
});
$layer.html(html);
},
renderConnections: function() {
var self = this;
var $layer = $(this.svgElement).find('.schematic-connections-layer');
$layer.empty();
var html = '';
var renderedCount = 0;
this.connections.forEach(function(conn, connIndex) {
// Check is_rail as integer (PHP may return string "1" or "0")
if (parseInt(conn.is_rail) === 1) {
return;
}
var sourceEq = conn.fk_source ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_source); }) : null;
var targetEq = conn.fk_target ? self.equipment.find(function(e) { return String(e.id) === String(conn.fk_target); }) : null;
var color = conn.color || self.PHASE_COLORS[conn.connection_type] || self.COLORS.connection;
// ========================================
// ABGANG (Output) - source exists, no target
// Direction depends on terminal: top terminal = line goes UP, bottom terminal = line goes DOWN
// ========================================
if (sourceEq && !conn.fk_target) {
var sourceTerminals = self.getTerminals(sourceEq);
var sourceTermId = conn.source_terminal_id || 't2';
var sourcePos = self.getTerminalPosition(sourceEq, sourceTermId, sourceTerminals);
if (!sourcePos) {
// Fallback: center bottom of equipment
sourcePos = {
x: sourceEq._x + (sourceEq._width || sourceEq.width_te * self.TE_WIDTH) / 2,
y: sourceEq._y + (sourceEq._height || 60) + 5,
isTop: false
};
}
// Calculate line length based on label text
var labelText = conn.output_label || '';
var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || '');
var maxTextLen = Math.max(labelText.length, cableText.trim().length);
var lineLength = Math.min(120, Math.max(50, maxTextLen * 6 + 20));
// Direction: top terminal = UP, bottom terminal = DOWN
var goingUp = sourcePos.isTop;
var startY = sourcePos.y;
var endY = goingUp ? (startY - lineLength) : (startY + lineLength);
// Draw vertical line
var path = 'M ' + sourcePos.x + ' ' + startY + ' L ' + sourcePos.x + ' ' + endY;
html += '';
// Invisible hit area for clicking
var hitY = goingUp ? endY : startY;
html += '';
// Connection line
html += '';
// Arrow at end (pointing away from equipment)
if (goingUp) {
// Arrow pointing UP
html += '';
} else {
// Arrow pointing DOWN
html += '';
}
// Labels - vertical text on both sides
var labelY = (startY + endY) / 2;
// Left side: Bezeichnung (output_label)
if (conn.output_label) {
html += '';
html += self.escapeHtml(conn.output_label);
html += '';
}
// Right side: Kabeltyp + Größe
var cableInfo = '';
if (conn.medium_type) cableInfo = conn.medium_type;
if (conn.medium_spec) cableInfo += ' ' + conn.medium_spec;
if (cableInfo) {
html += '';
html += self.escapeHtml(cableInfo.trim());
html += '';
}
// Phase type at end of line
if (conn.connection_type) {
var phaseY = goingUp ? (endY - 10) : (endY + 14);
html += '';
html += conn.connection_type;
html += '';
}
html += '';
renderedCount++;
return;
}
// ========================================
// ANSCHLUSSPUNKT (Input) - no source, target exists
// Draw a line coming FROM ABOVE into the terminal
// All Anschlusspunkte use a uniform color (light blue)
// ========================================
if (!conn.fk_source && targetEq) {
var targetTerminals = self.getTerminals(targetEq);
var targetTermId = conn.target_terminal_id || 't1';
var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals);
if (!targetPos) return;
// Uniform color for all Anschlusspunkte (inputs)
var inputColor = '#4fc3f7'; // Light blue - uniform for all inputs
// Calculate line length based on label
var inputLabel = conn.output_label || '';
var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30));
var startY = targetPos.y - inputLineLength;
// Draw vertical line coming down into terminal
var path = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y;
html += '';
// Invisible hit area for clicking
html += '';
// Connection line
html += '';
// Circle at top (external source indicator)
html += '';
// Arrow pointing down into terminal
html += '';
// Phase label at top (big, prominent)
html += '';
html += conn.connection_type || 'L1';
html += '';
// Optional label on side (vertical)
if (conn.output_label) {
var labelY = targetPos.y - inputLineLength / 2;
html += '';
html += self.escapeHtml(conn.output_label);
html += '';
}
html += '';
renderedCount++;
return;
}
// ========================================
// NORMAL CONNECTION - both source and target exist
// ========================================
if (!sourceEq || !targetEq) {
return;
}
var sourceTerminals = self.getTerminals(sourceEq);
var targetTerminals = self.getTerminals(targetEq);
var sourceTermId = conn.source_terminal_id || 't2';
var targetTermId = conn.target_terminal_id || 't1';
var sourcePos = self.getTerminalPosition(sourceEq, sourceTermId, sourceTerminals);
var targetPos = self.getTerminalPosition(targetEq, targetTermId, targetTerminals);
if (!sourcePos || !targetPos) return;
var routeOffset = connIndex * 8;
var path;
if (conn.path_data) {
path = conn.path_data;
} else {
path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq);
}
html += '';
html += '';
html += '';
html += '';
if (conn.output_label) {
var labelX = (sourcePos.x + targetPos.x) / 2;
var labelY = sourcePos.isTop ? sourcePos.y - 70 - routeOffset : sourcePos.y + 70 + routeOffset;
var labelWidth = Math.min(conn.output_label.length * 8 + 14, 112);
html += '';
html += '';
html += self.escapeHtml(conn.output_label);
html += '';
}
if (conn.connection_type && !conn.output_label) {
var typeX = (sourcePos.x + targetPos.x) / 2;
var typeY = (sourcePos.y + targetPos.y) / 2;
html += '';
html += conn.connection_type;
html += '';
}
html += '';
renderedCount++;
});
$layer.html(html);
// Bind click events to SVG connection elements (must be done after rendering)
var self = this;
// Normal connections
$layer.find('.schematic-connection-group').each(function() {
var $group = $(this);
var connId = $group.data('connection-id');
var $visiblePath = $group.find('.schematic-connection');
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (connId) {
self.showConnectionPopup(connId, e.clientX, e.clientY);
}
});
this.addEventListener('mouseenter', function() {
$visiblePath.attr('stroke-width', '5');
});
this.addEventListener('mouseleave', function() {
$visiblePath.attr('stroke-width', '3.5');
});
this.style.cursor = 'pointer';
});
// Abgang (Output) groups - click to edit
$layer.find('.schematic-output-group').each(function() {
var $group = $(this);
var connId = $group.data('connection-id');
var $visiblePath = $group.find('.schematic-connection');
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (connId) {
self.showConnectionPopup(connId, e.clientX, e.clientY);
}
});
this.addEventListener('mouseenter', function() {
$visiblePath.attr('stroke-width', '5');
});
this.addEventListener('mouseleave', function() {
$visiblePath.attr('stroke-width', '3');
});
});
// Anschlusspunkt (Input) groups - click to edit
$layer.find('.schematic-input-group').each(function() {
var $group = $(this);
var connId = $group.data('connection-id');
var $visiblePath = $group.find('.schematic-connection');
this.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (connId) {
self.showConnectionPopup(connId, e.clientX, e.clientY);
}
});
this.addEventListener('mouseenter', function() {
$visiblePath.attr('stroke-width', '5');
});
this.addEventListener('mouseleave', function() {
$visiblePath.attr('stroke-width', '3');
});
});
},
renderControls: function() {
var self = this;
var $canvas = $('.schematic-editor-canvas');
// Remove existing controls
$canvas.find('.schematic-controls').remove();
// Find wrapper or canvas for control placement
var $wrapper = $canvas.find('.schematic-zoom-wrapper');
var $controlsParent = $wrapper.length ? $wrapper : $canvas;
// Create controls container (positioned absolute over SVG)
var $controls = $('');
// Button style
var btnStyle = 'pointer-events:auto;width:28px;height:28px;border-radius:50%;border:2px solid #3498db;' +
'background:#2d2d44;color:#3498db;font-size:18px;font-weight:bold;cursor:pointer;' +
'display:flex;align-items:center;justify-content:center;transition:all 0.2s;';
var btnHoverStyle = 'background:#3498db;color:#fff;';
// Add Panel button (right of last panel)
if (this.panels.length > 0) {
var lastPanel = this.panels[this.panels.length - 1];
var addPanelX = (lastPanel._x || 0) + (lastPanel._width || 200) + 20;
var addPanelY = this.panelTopMargin + 50;
var $addPanel = $('');
$controls.append($addPanel);
// Copy Panel button (if more than one panel exists)
if (this.panels.length >= 1) {
var $copyPanel = $('');
$controls.append($copyPanel);
}
} else {
// No panels - show add panel button
var $addPanel = $('');
$controls.append($addPanel);
}
// Add Carrier button (below each panel's last carrier, or at top if no carriers)
this.panels.forEach(function(panel) {
var panelCarriers = self.carriers.filter(function(c) { return c.panel_id == panel.id; });
var lastCarrier = panelCarriers[panelCarriers.length - 1];
if (lastCarrier && lastCarrier._y) {
var addCarrierY = lastCarrier._y + self.RAIL_HEIGHT + 30;
var addCarrierX = (panel._x || 0) + 60;
var $addCarrier = $('');
$controls.append($addCarrier);
// Copy Carrier button
if (panelCarriers.length >= 1) {
var $copyCarrier = $('');
$controls.append($copyCarrier);
}
} else {
// No carriers in this panel - show add carrier button at top
var addCarrierY = self.calculatedTopMargin + 20;
var addCarrierX = (panel._x || 0) + 60;
var $addCarrier = $('');
$controls.append($addCarrier);
// Delete empty panel button
var $deletePanel = $('');
$controls.append($deletePanel);
}
});
// Add Equipment button & Copy button (next to last equipment on each carrier)
this.carriers.forEach(function(carrier) {
var carrierEquipment = self.equipment.filter(function(e) { return String(e.carrier_id) === String(carrier.id); });
// Check if carrier has position data (use typeof to allow 0 values)
if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') {
// Belegte Ranges ermitteln (Dezimal-TE-Unterstützung)
var totalTE = parseFloat(carrier.total_te) || 12;
var lastEquipment = null;
var lastEndPos = 0;
var ranges = [];
carrierEquipment.forEach(function(eq) {
var pos = parseFloat(eq.position_te) || 1;
var w = parseFloat(eq.width_te) || 1;
var endPos = pos + w;
ranges.push({ start: pos, end: endPos });
if (endPos > lastEndPos) {
lastEndPos = endPos;
lastEquipment = eq;
}
});
ranges.sort(function(a, b) { return a.start - b.start; });
// Maximale zusammenhängende Lücke berechnen
var maxGap = 0;
var gapPos = 1;
var railEnd = totalTE + 1;
ranges.forEach(function(r) {
var gap = r.start - gapPos;
if (gap > maxGap) maxGap = gap;
if (r.end > gapPos) gapPos = r.end;
});
var endGap = railEnd - gapPos;
if (endGap > maxGap) maxGap = endGap;
// Carrier-Objekt merkt sich maximale Lücke für Typ-Filter
carrier._maxGap = maxGap;
// Calculate button positions - place buttons on the LEFT side of the carrier
// Rail label is at about carrier._x - 44, so buttons need to be further left
var btnX = carrier._x - 100; // Left of the carrier and rail label
var btnY = carrier._y - self.BLOCK_HEIGHT / 2 + 10; // Aligned with blocks
if (maxGap > 0) {
// Add Equipment button - positioned left of carrier
var $addEquipment = $('');
$controls.append($addEquipment);
// Copy Equipment button (below the + button)
if (lastEquipment) {
var copyBtnY = btnY + 30;
var $copyEquipment = $('');
$controls.append($copyEquipment);
}
}
// Add Busbar button (below copy button or + button) - now blue and round like other buttons
var busbarBtnX = btnX;
var busbarBtnY = btnY + (lastEquipment ? 60 : 30);
var $addBusbar = $('');
$controls.append($addBusbar);
} else {
console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position');
}
});
// Append controls to wrapper (if exists) or canvas
$canvas.css('position', 'relative');
$controlsParent.css('position', 'relative').append($controls);
// Bind control events
this.bindControlEvents();
},
bindControlEvents: function() {
var self = this;
// Add Panel
$(document).off('click.addPanel').on('click.addPanel', '.schematic-add-panel', function(e) {
e.preventDefault();
self.showAddPanelDialog();
});
// Copy Panel
$(document).off('click.copyPanel').on('click.copyPanel', '.schematic-copy-panel', function(e) {
e.preventDefault();
var panelId = $(this).data('panel-id');
self.duplicatePanel(panelId);
});
// Delete empty Panel
$(document).off('click.deletePanel').on('click.deletePanel', '.schematic-delete-panel', function(e) {
e.preventDefault();
var panelId = $(this).data('panel-id');
self.deletePanel(panelId);
});
// Add Carrier
$(document).off('click.addCarrier').on('click.addCarrier', '.schematic-add-carrier', function(e) {
e.preventDefault();
var panelId = $(this).data('panel-id');
self.showAddCarrierDialog(panelId);
});
// Copy Carrier
$(document).off('click.copyCarrier').on('click.copyCarrier', '.schematic-copy-carrier', function(e) {
e.preventDefault();
var carrierId = $(this).data('carrier-id');
self.duplicateCarrier(carrierId);
});
// Add Equipment
$(document).off('click.addEquipment').on('click.addEquipment', '.schematic-add-equipment', function(e) {
e.preventDefault();
var carrierId = $(this).data('carrier-id');
// Maximale Lücke vom Carrier holen für Typ-Filter
var carrier = self.carriers.find(function(c) { return String(c.id) === String(carrierId); });
var maxGap = carrier ? (carrier._maxGap || 99) : 99;
self.showAddEquipmentDialog(carrierId, maxGap);
});
// Copy Equipment (single copy)
$(document).off('click.copyEquipment').on('click.copyEquipment', '.schematic-copy-equipment', function(e) {
e.preventDefault();
var equipmentId = $(this).data('equipment-id');
self.duplicateSingleEquipment(equipmentId);
});
// Add Busbar (Sammelschiene)
$(document).off('click.addBusbar').on('click.addBusbar', '.schematic-add-busbar', function(e) {
e.preventDefault();
var carrierId = $(this).data('carrier-id');
self.showAddBusbarDialog(carrierId);
});
// Hover effects
$('.schematic-controls button').hover(
function() { $(this).css({ background: '#3498db', color: '#fff' }); },
function() { $(this).css({ background: '#2d2d44', color: '#3498db' }); }
);
// Special hover for busbar button
$('.schematic-add-busbar').hover(
function() { $(this).css({ background: '#e74c3c', color: '#fff' }); },
function() { $(this).css({ background: '#2d2d44', color: '#e74c3c' }); }
);
},
showAddPanelDialog: function() {
var self = this;
var html = '';
html += '
';
$('body').append(popupHtml);
// Bind button events
$('.schematic-eq-popup-edit').on('click', function(e) {
e.stopPropagation();
self.editEquipment(equipmentId);
});
$('.schematic-eq-popup-delete').on('click', function(e) {
e.stopPropagation();
self.hideEquipmentPopup();
self.deleteEquipment(equipmentId);
});
// Adjust position if popup goes off screen
var $popup = $('.schematic-equipment-popup');
var popupWidth = $popup.outerWidth();
var popupHeight = $popup.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
if (x + popupWidth > windowWidth - 10) {
$popup.css('left', (x - popupWidth) + 'px');
}
if (y + popupHeight > windowHeight - 10) {
$popup.css('top', (y - popupHeight) + 'px');
}
},
hideEquipmentPopup: function() {
$('.schematic-equipment-popup').remove();
},
// Block Hover Tooltip mit show_in_hover Feldern
showBlockTooltip: function(equipmentId, e) {
var self = this;
// Hide any existing tooltip
this.hideBlockTooltip();
// Find equipment data
var eq = this.equipment.find(function(equ) { return parseInt(equ.id) === parseInt(equipmentId); });
if (!eq) return;
// Build tooltip content
var html = '
';
// Header: Type + Label
html += '
';
html += '' + self.escapeHtml(eq.type_label || eq.type_ref || '') + '';
if (eq.label) {
html += ' ' + self.escapeHtml(eq.label) + '';
}
html += '
';
// Show fields with show_in_hover = 1
var hasHoverFields = false;
if (eq.type_fields && eq.field_values) {
eq.type_fields.forEach(function(field) {
if (field.show_in_hover && eq.field_values[field.field_code]) {
hasHoverFields = true;
var val = eq.field_values[field.field_code];
// Add units for known fields
if (field.field_code === 'ampere') val += ' A';
else if (field.field_code === 'sensitivity') val += ' mA';
else if (field.field_code === 'voltage') val += ' V';
html += '
';
html += '' + self.escapeHtml(field.field_label) + ':';
html += '' + self.escapeHtml(val) + '';
html += '
';
}
});
}
// Position info
html += '
';
html += 'Position:';
html += '' + (eq.position_te || 1) + ' TE';
html += '
';
html += '
';
html += 'Breite:';
html += '' + (eq.width_te || 1) + ' TE';
html += '
';
// Product info if assigned
if (eq.fk_product && eq.product_ref) {
html += '
';
html += '
';
html += '';
html += '' + self.escapeHtml(eq.product_ref) + '';
html += '
';
if (eq.product_label) {
html += '
' + self.escapeHtml(eq.product_label) + '
';
}
html += '
';
}
html += '
';
$('body').append(html);
this.updateBlockTooltipPosition(e);
},
updateBlockTooltipPosition: function(e) {
var $tooltip = $('#schematic-block-tooltip');
if (!$tooltip.length) return;
var x = e.clientX + 15;
var y = e.clientY + 15;
// Keep tooltip on screen
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
if (x + tooltipWidth > windowWidth - 10) {
x = e.clientX - tooltipWidth - 10;
}
if (y + tooltipHeight > windowHeight - 10) {
y = e.clientY - tooltipHeight - 10;
}
$tooltip.css({ left: x + 'px', top: y + 'px' });
},
hideBlockTooltip: function() {
$('#schematic-block-tooltip').remove();
},
showCarrierPopup: function(carrierId, x, y) {
var self = this;
// Remove any existing popup
this.hideCarrierPopup();
this.hideEquipmentPopup();
this.hideConnectionPopup();
// Find carrier data
var carrier = this.carriers.find(function(c) { return parseInt(c.id) === parseInt(carrierId); });
if (!carrier) {
console.warn('Carrier not found for id:', carrierId);
return;
}
// Check if carrier has equipment
var carrierEquipment = this.equipment.filter(function(e) { return parseInt(e.fk_carrier) === parseInt(carrierId); });
var isEmpty = carrierEquipment.length === 0;
var deleteStyle = isEmpty ? 'background:#e74c3c;' : 'background:#888;';
// Create popup
var popupHtml = '
';
$('body').append(dialogHtml);
// Bind events
$('.edit-dialog-cancel, .schematic-edit-overlay').on('click', function() {
$('.schematic-edit-dialog, .schematic-edit-overlay').remove();
});
$('.edit-dialog-save').on('click', function() {
self.saveEquipmentEdit(eq.id);
});
// Enter key to save
$('.schematic-edit-dialog input').on('keypress', function(e) {
if (e.which === 13) {
self.saveEquipmentEdit(eq.id);
}
});
// Lade Felder für aktuellen Typ
var existingValues = {};
try {
existingValues = eq.field_values ? (typeof eq.field_values === 'string' ? JSON.parse(eq.field_values) : eq.field_values) : {};
} catch(e) { existingValues = {}; }
var allTypes = types;
self.loadTypeFields(eq.type_id, null, '.edit-type-fields', existingValues);
// Bei Typ-Änderung Felder neu laden
$('.edit-equipment-type').on('change', function() {
var typeId = $(this).val();
self.loadTypeFields(typeId, null, '.edit-type-fields', {});
});
// Produktsuche mit Autocomplete initialisieren
self.initProductAutocomplete('.edit-product-search', '.edit-product-id', '.edit-product-clear', eq.fk_product);
},
saveEquipmentEdit: function(equipmentId) {
var self = this;
// Sammle Feldwerte
var fieldValues = {};
$('.edit-type-fields').find('input, select').each(function() {
var code = $(this).data('field-code');
if (code) {
fieldValues[code] = $(this).val();
}
});
var data = {
action: 'update',
equipment_id: equipmentId,
type_id: $('.edit-equipment-type').val(),
label: $('.edit-equipment-label').val(),
position_te: $('.edit-equipment-position').val(),
field_values: JSON.stringify(fieldValues),
fk_product: $('.edit-product-id').val() || 0,
token: $('input[name="token"]').val()
};
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
method: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.success) {
$('.schematic-edit-dialog, .schematic-edit-overlay').remove();
self.showMessage('Equipment aktualisiert', 'success');
self.loadData();
} else {
self.showMessage(response.error || 'Fehler beim Speichern', 'error');
}
},
error: function() {
self.showMessage('Netzwerkfehler', 'error');
}
});
},
deleteEquipment: function(equipmentId) {
var self = this;
this.hideEquipmentPopup();
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
method: 'POST',
data: {
action: 'delete',
equipment_id: equipmentId,
token: $('input[name="token"]').val()
},
dataType: 'json',
success: function(response) {
if (response.success) {
self.showMessage('Equipment gelöscht', 'success');
// Remove from local array instead of reloading everything
self.equipment = self.equipment.filter(function(e) {
return String(e.id) !== String(equipmentId);
});
// Also remove connections involving this equipment
self.connections = self.connections.filter(function(c) {
return String(c.fk_source) !== String(equipmentId) &&
String(c.fk_target) !== String(equipmentId);
});
self.render();
} else {
self.showMessage(response.error || 'Fehler', 'error');
}
}
});
},
// Block dragging
startDragBlock: function($block, e) {
var eqId = $block.data('equipment-id');
var carrierId = $block.data('carrier-id');
var eq = this.equipment.find(function(e) { return String(e.id) === String(eqId); });
var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); });
if (!eq || !carrier) {
console.log('startDragBlock: Equipment or carrier not found', eqId, carrierId);
return;
}
if (typeof carrier._x === 'undefined') {
console.log('startDragBlock: Carrier has no _x position', carrier);
return;
}
var $svg = $(this.svgElement);
var offset = $svg.offset();
this.dragState = {
type: 'block',
equipmentId: eqId,
equipment: eq,
carrier: carrier,
element: $block,
startX: e.pageX,
startY: e.pageY,
originalTE: parseFloat(eq.position_te) || 1,
originalX: parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2,
originalY: eq._y
};
$block.addClass('dragging');
},
updateDragBlock: function(e) {
if (!this.dragState || this.dragState.type !== 'block') return;
var dx = e.pageX - this.dragState.startX;
var dy = e.pageY - this.dragState.startY;
// Find target carrier based on mouse position (supports cross-panel drag)
var $svg = $(this.svgElement);
var svgOffset = $svg.offset();
var mouseY = (e.pageY - svgOffset.top) / this.scale;
var mouseX = (e.pageX - svgOffset.left) / this.scale;
// Find closest carrier (checks both Y and X for cross-panel support)
var targetCarrier = this.findClosestCarrier(mouseY, mouseX);
if (!targetCarrier) targetCarrier = this.dragState.carrier;
// TE-Position auf Ziel-Carrier berechnen (0.1er-Snap für Dezimal-Breiten)
var relativeX = mouseX - targetCarrier._x;
var rawTE = relativeX / this.TE_WIDTH + 1;
var newTE = Math.round(rawTE * 10) / 10;
// Auf gültigen Bereich auf ZIEL-Carrier begrenzen
var maxTE = (parseFloat(targetCarrier.total_te) || 12) - (parseFloat(this.dragState.equipment.width_te) || 1) + 1;
newTE = Math.max(1, Math.min(newTE, Math.round(maxTE * 10) / 10));
// Calculate visual position
var newX, newY;
if (targetCarrier.id === this.dragState.carrier.id) {
// Same carrier - use offset from original
var teOffset = newTE - this.dragState.originalTE;
newX = this.dragState.originalX + teOffset * this.TE_WIDTH;
newY = this.dragState.originalY;
} else {
// Different carrier - calculate new position
newX = parseFloat(targetCarrier._x) + (newTE - 1) * this.TE_WIDTH + 2;
// Block Y position: carrier Y - half block height (block sits on rail)
newY = parseFloat(targetCarrier._y) - this.BLOCK_HEIGHT / 2;
}
if (!isNaN(newX) && !isNaN(newY)) {
this.dragState.element.attr('transform', 'translate(' + newX + ',' + newY + ')');
}
// Store target info
this.dragState.newTE = newTE;
this.dragState.targetCarrier = targetCarrier;
// Highlight target carrier
this.highlightTargetCarrier(targetCarrier.id);
},
findClosestCarrier: function(mouseY, mouseX) {
var self = this;
var closest = null;
var closestDist = Infinity;
this.carriers.forEach(function(carrier) {
if (typeof carrier._y === 'undefined' || typeof carrier._x === 'undefined') return;
var carrierY = parseFloat(carrier._y);
var carrierX = parseFloat(carrier._x);
var carrierWidth = (carrier.total_te || 12) * self.TE_WIDTH;
// Calculate distance - prioritize Y but also check X is within carrier bounds
var distY = Math.abs(mouseY - carrierY);
// Check if mouseX is within carrier X range (with some tolerance)
var tolerance = 50;
var inXRange = mouseX >= (carrierX - tolerance) && mouseX <= (carrierX + carrierWidth + tolerance);
// Use combined distance for carriers in range, otherwise penalize
var dist = inXRange ? distY : distY + 1000;
if (dist < closestDist) {
closestDist = dist;
closest = carrier;
}
});
// Only return if within reasonable distance (half rail spacing)
if (closestDist < this.RAIL_SPACING / 2) {
return closest;
}
return null;
},
highlightTargetCarrier: function(carrierId) {
// Remove previous highlights
$('.schematic-rail').removeClass('drop-target');
// Add highlight to target
$('.schematic-rail[data-carrier-id="' + carrierId + '"]').addClass('drop-target');
},
endDragBlock: function(e) {
if (!this.dragState || this.dragState.type !== 'block') return;
var newTE = this.dragState.newTE || this.dragState.originalTE;
var element = this.dragState.element;
var targetCarrier = this.dragState.targetCarrier || this.dragState.carrier;
var originalCarrier = this.dragState.carrier;
// Remove highlight
$('.schematic-rail').removeClass('drop-target');
// Check if carrier changed or position changed
var carrierChanged = String(targetCarrier.id) !== String(originalCarrier.id);
var positionChanged = newTE !== this.dragState.originalTE;
console.log('endDragBlock: carrierChanged=', carrierChanged, 'positionChanged=', positionChanged,
'newTE=', newTE, 'originalTE=', this.dragState.originalTE,
'targetCarrier=', targetCarrier.id, 'originalCarrier=', originalCarrier.id);
if (carrierChanged) {
// Move to different carrier
this.moveEquipmentToCarrier(this.dragState.equipment.id, targetCarrier.id, newTE);
} else if (positionChanged) {
// Same carrier, different position
this.updateEquipmentPosition(this.dragState.equipment.id, newTE);
} else {
// No change, reset visual position
console.log('No change detected, resetting position');
element.attr('transform', 'translate(' + this.dragState.originalX + ',' + this.dragState.originalY + ')');
}
element.removeClass('dragging');
this.dragState = null;
},
moveEquipmentToCarrier: function(eqId, newCarrierId, newTE) {
var self = this;
console.log('moveEquipmentToCarrier:', eqId, 'to carrier', newCarrierId, 'position', newTE);
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
method: 'POST',
data: {
action: 'move_to_carrier',
equipment_id: eqId,
carrier_id: newCarrierId,
position_te: newTE,
token: $('input[name="token"]').val()
},
dataType: 'json',
success: function(response) {
console.log('moveEquipmentToCarrier response:', response);
if (response.success) {
self.showMessage('Equipment verschoben', 'success');
self.loadData(); // Reload to update all positions
} else {
self.showMessage(response.error || 'Fehler beim Verschieben', 'error');
self.render();
}
},
error: function(xhr, status, error) {
console.log('moveEquipmentToCarrier error:', status, error, xhr.responseText);
self.showMessage('Netzwerkfehler', 'error');
self.render();
}
});
},
updateEquipmentPosition: function(eqId, newTE) {
var self = this;
console.log('updateEquipmentPosition:', eqId, newTE);
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
method: 'POST',
data: {
action: 'update_position',
equipment_id: eqId,
position_te: newTE,
token: $('input[name="token"]').val()
},
dataType: 'json',
success: function(response) {
if (response.success) {
// Update local data and re-render
var eq = self.equipment.find(function(e) { return String(e.id) === String(eqId); });
if (eq) {
eq.position_te = newTE;
}
// Full re-render to ensure carrier positions are set
self.render();
} else {
// Revert visual position on error
self.showMessage(response.error || 'Position nicht verfügbar', 'error');
self.render();
}
},
error: function() {
// Revert on error
self.showMessage('Netzwerkfehler', 'error');
self.render();
}
});
},
// Zoom functions
setZoom: function(newScale) {
// Clamp zoom level between 0.25 and 2.0
this.scale = Math.max(0.25, Math.min(2.0, newScale));
// Apply transform to wrapper (SVG + controls)
var $wrapper = $('.schematic-zoom-wrapper');
if ($wrapper.length) {
$wrapper.css('transform', 'scale(' + this.scale + ')');
$wrapper.css('transform-origin', '0 0');
// Update container size to account for scaling
var $svg = $wrapper.find('.schematic-svg');
if ($svg.length) {
var actualWidth = $svg.attr('width') * this.scale;
var actualHeight = $svg.attr('height') * this.scale;
$wrapper.parent().css({
'min-width': actualWidth + 'px',
'min-height': actualHeight + 'px'
});
}
}
// Update zoom display
this.updateZoomDisplay();
},
zoomToFit: function() {
if (!this.svgElement) return;
var $canvas = $('.schematic-editor-canvas');
var $svg = $(this.svgElement);
var canvasWidth = $canvas.width() - 40; // Padding
var canvasHeight = $canvas.height() - 40;
var svgWidth = parseFloat($svg.attr('width')) || 800;
var svgHeight = parseFloat($svg.attr('height')) || 600;
// Calculate scale to fit both dimensions
var scaleX = canvasWidth / svgWidth;
var scaleY = canvasHeight / svgHeight;
var newScale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 100%
this.setZoom(newScale);
},
updateZoomDisplay: function() {
var percentage = Math.round(this.scale * 100);
$('.schematic-zoom-level').text(percentage + '%');
// Show zoom level in status bar
this.showMessage('Zoom: ' + percentage + '%', 'info');
},
showMessage: function(text, type) {
var $msg = $('.schematic-message');
if (!$msg.length) return; // Statuszeile wird in PHP erstellt
// Clear any pending hide timeout
if (this.messageTimeout) {
clearTimeout(this.messageTimeout);
this.messageTimeout = null;
}
// Atomarer Klassen-Wechsel - kein Flackern durch removeClass/addClass
$msg.attr('class', 'schematic-message ' + type).text(text);
if (type === 'success' || type === 'error') {
this.messageTimeout = setTimeout(function() {
$msg.attr('class', 'schematic-message info').text('Bereit');
}, 2500);
}
},
hideMessage: function() {
var $msg = $('.schematic-message');
$msg.attr('class', 'schematic-message info').text('Bereit');
},
escapeHtml: function(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on DOM ready
$(document).ready(function() {
KundenKarte.Tree.init();
KundenKarte.Favorites.init();
KundenKarte.SystemTabs.init();
KundenKarte.IconPicker.init();
KundenKarte.DynamicFields.init();
// Initialize Equipment if container exists
var $equipmentContainer = $('.kundenkarte-equipment-container');
if ($equipmentContainer.length) {
var anlageId = $equipmentContainer.data('anlage-id');
var systemId = $equipmentContainer.data('system-id');
KundenKarte.Equipment.init(anlageId, systemId);
// Initialize Schematic Editor
KundenKarte.SchematicEditor.init(anlageId);
}
// Initialize Anlage Connection handlers
KundenKarte.AnlageConnection.init();
});
// ===========================================
// Anlage Connections (cable connections between tree elements)
// ===========================================
KundenKarte.AnlageConnection = {
baseUrl: '',
init: function() {
var self = this;
this.baseUrl = (typeof baseUrl !== 'undefined') ? baseUrl : '';
this.tooltipTimer = null;
// Create tooltip container if not exists
if ($('#kundenkarte-conn-tooltip').length === 0) {
$('body').append('');
}
// Add connection button
$(document).on('click', '.anlage-connection-add', function(e) {
e.preventDefault();
var anlageId = $(this).data('anlage-id');
var socId = $(this).data('soc-id');
var systemId = $(this).data('system-id');
self.showConnectionDialog(anlageId, socId, systemId, null);
});
// Edit connection (old style)
$(document).on('click', '.anlage-connection-edit', function(e) {
e.preventDefault();
var connId = $(this).data('id');
self.loadAndEditConnection(connId);
});
// Delete connection (old style)
$(document).on('click', '.anlage-connection-delete', function(e) {
e.preventDefault();
var connId = $(this).data('id');
self.deleteConnection(connId);
});
// Hover on connection row - show tooltip
$(document).on('mouseenter', '.kundenkarte-tree-conn', function(e) {
var $el = $(this);
var tooltipData = $el.data('conn-tooltip');
if (tooltipData) {
self.showConnTooltip(e, tooltipData);
}
});
$(document).on('mousemove', '.kundenkarte-tree-conn', function(e) {
self.moveConnTooltip(e);
});
$(document).on('mouseleave', '.kundenkarte-tree-conn', function() {
self.hideConnTooltip();
});
},
showConnTooltip: function(e, data) {
var $tooltip = $('#kundenkarte-conn-tooltip');
var html = '
';
html += '';
html += '' + this.escapeHtml(data.source || '') + ' → ' + this.escapeHtml(data.target || '') + '';
html += '
';
html += '
';
if (data.medium_type) {
html += 'Kabeltyp:';
html += '' + this.escapeHtml(data.medium_type) + '';
}
if (data.medium_spec) {
html += 'Querschnitt:';
html += '' + this.escapeHtml(data.medium_spec) + '';
}
if (data.medium_length) {
html += 'Länge:';
html += '' + this.escapeHtml(data.medium_length) + '';
}
if (data.medium_color) {
html += 'Farbe:';
html += '' + this.escapeHtml(data.medium_color) + '';
}
if (data.route_description) {
html += 'Route:';
html += '' + this.escapeHtml(data.route_description) + '';
}
if (data.installation_date) {
html += 'Installiert:';
html += '' + this.escapeHtml(data.installation_date) + '';
}
if (data.label) {
html += 'Beschreibung:';
html += '' + this.escapeHtml(data.label) + '';
}
html += '
';
html += '
Klicken zum Bearbeiten
';
$tooltip.html(html);
this.moveConnTooltip(e);
$tooltip.addClass('visible');
},
moveConnTooltip: function(e) {
var $tooltip = $('#kundenkarte-conn-tooltip');
var x = e.pageX + 15;
var y = e.pageY + 10;
// Keep tooltip in viewport
var tooltipWidth = $tooltip.outerWidth();
var tooltipHeight = $tooltip.outerHeight();
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var scrollTop = $(window).scrollTop();
if (x + tooltipWidth > windowWidth - 20) {
x = e.pageX - tooltipWidth - 15;
}
if (y + tooltipHeight > scrollTop + windowHeight - 20) {
y = e.pageY - tooltipHeight - 10;
}
$tooltip.css({ left: x, top: y });
},
hideConnTooltip: function() {
$('#kundenkarte-conn-tooltip').removeClass('visible');
},
loadAndEditConnection: function(connId) {
var self = this;
console.log('loadAndEditConnection called with connId:', connId);
$.ajax({
url: this.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
data: { action: 'get', connection_id: connId },
dataType: 'json',
success: function(response) {
console.log('Connection fetch response:', response);
if (response.success) {
// Get soc_id and system_id from the tree container or add button
var $tree = $('.kundenkarte-tree');
var socId = $tree.data('socid');
var systemId = $tree.data('system');
console.log('From tree element - socId:', socId, 'systemId:', systemId, '$tree.length:', $tree.length);
// Fallback to add button if tree doesn't have data
if (!socId) {
var $btn = $('.anlage-connection-add').first();
socId = $btn.data('soc-id');
systemId = $btn.data('system-id');
console.log('From fallback button - socId:', socId, 'systemId:', systemId);
}
self.showConnectionDialog(response.connection.fk_source, socId, systemId, response.connection);
} else {
KundenKarte.showAlert('Fehler', response.error || 'Fehler beim Laden');
}
}
});
},
showConnectionDialog: function(anlageId, socId, systemId, existingConn) {
var self = this;
console.log('showConnectionDialog called:', {anlageId: anlageId, socId: socId, systemId: systemId, existingConn: existingConn});
// Ensure systemId is a number (0 if not set)
systemId = systemId || 0;
// First load anlagen list and medium types
$.when(
$.ajax({
url: this.baseUrl + '/custom/kundenkarte/ajax/anlage.php',
data: { action: 'tree', socid: socId, system_id: systemId },
dataType: 'json'
}),
$.ajax({
url: this.baseUrl + '/custom/kundenkarte/ajax/medium_types.php',
data: { action: 'list_grouped', system_id: systemId },
dataType: 'json'
})
).done(function(anlagenResp, mediumResp) {
console.log('AJAX responses:', {anlagenResp: anlagenResp, mediumResp: mediumResp});
var anlagen = anlagenResp[0].success ? anlagenResp[0].tree : [];
var mediumGroups = mediumResp[0].success ? mediumResp[0].groups : [];
console.log('Parsed data:', {anlagen: anlagen, mediumGroups: mediumGroups});
self.renderConnectionDialog(anlageId, anlagen, mediumGroups, existingConn);
});
},
flattenTree: function(tree, result, prefix) {
result = result || [];
prefix = prefix || '';
for (var i = 0; i < tree.length; i++) {
var node = tree[i];
result.push({
id: node.id,
label: prefix + (node.label || node.ref),
ref: node.ref
});
if (node.children && node.children.length > 0) {
this.flattenTree(node.children, result, prefix + ' ');
}
}
return result;
},
renderConnectionDialog: function(anlageId, anlagenTree, mediumGroups, existingConn) {
var self = this;
console.log('renderConnectionDialog - anlagenTree:', anlagenTree);
var anlagen = this.flattenTree(anlagenTree);
console.log('renderConnectionDialog - flattened anlagen:', anlagen);
// Remove existing dialog
$('#anlage-connection-dialog').remove();
var isEdit = existingConn !== null;
var title = isEdit ? 'Verbindung bearbeiten' : 'Neue Verbindung';
var html = '
';
html += '
';
html += '
' + title + '
';
html += '×
';
html += '
';
// Source
html += '
';
html += '';
html += '
';
// Target
html += '
';
html += '';
html += '
';
// Medium type
html += '
';
html += '';
html += '';
html += '';
html += '
';
// Spec
html += '
';
html += '';
html += '';
html += '';
html += '
';
// Length
html += '
';
html += '';
html += '';
html += '
';
// Label
html += '
';
html += '';
html += '';
html += '
';
// Route description
html += '
';
html += '';
html += '';
html += '
';
html += '
'; // modal-body
// Footer
html += '';
html += '
';
$('body').append(html);
// Spec dropdown update
$('.conn-medium-type').on('change', function() {
var $opt = $(this).find('option:selected');
var specs = $opt.data('specs') || [];
var defSpec = $opt.data('default') || '';
var $specSelect = $('.conn-spec');
$specSelect.empty().append('');
for (var i = 0; i < specs.length; i++) {
var sel = (existingConn && existingConn.medium_spec === specs[i]) || (!existingConn && specs[i] === defSpec) ? ' selected' : '';
$specSelect.append('');
}
});
// Trigger to populate specs if editing
if (existingConn && existingConn.fk_medium_type) {
$('.conn-medium-type').trigger('change');
if (existingConn.medium_spec) {
$('.conn-spec').val(existingConn.medium_spec);
}
}
// Close handlers
$('.conn-cancel, #anlage-connection-dialog .kundenkarte-modal-close').on('click', function() {
$('#anlage-connection-dialog').remove();
});
// Save handler
$('.conn-save').on('click', function() {
var data = {
fk_source: $('.conn-source').val(),
fk_target: $('.conn-target').val(),
fk_medium_type: $('.conn-medium-type').val(),
medium_type_text: $('.conn-medium-text').val(),
medium_spec: $('.conn-spec').val() || $('.conn-spec-text').val(),
medium_length: $('.conn-length').val(),
label: $('.conn-label').val(),
route_description: $('.conn-route').val(),
token: $('input[name="token"]').val()
};
if (!data.fk_target) {
KundenKarte.showAlert('Fehler', 'Bitte wählen Sie ein Ziel aus.');
return;
}
if (isEdit) {
data.action = 'update';
data.connection_id = existingConn.id;
} else {
data.action = 'create';
}
$.ajax({
url: self.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
method: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.success) {
$('#anlage-connection-dialog').remove();
location.reload();
} else {
KundenKarte.showAlert('Fehler', response.error || 'Speichern fehlgeschlagen');
}
}
});
});
},
deleteConnection: function(connId) {
var self = this;
KundenKarte.showConfirm('Verbindung löschen', 'Möchten Sie diese Verbindung wirklich löschen?', function(confirmed) {
if (confirmed) {
$.ajax({
url: self.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
method: 'POST',
data: {
action: 'delete',
connection_id: connId,
token: $('input[name="token"]').val()
},
dataType: 'json',
success: function(response) {
if (response.success) {
location.reload();
} else {
KundenKarte.showAlert('Fehler', response.error || 'Löschen fehlgeschlagen');
}
}
});
}
});
},
escapeHtml: function(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// ===========================================
// File Upload Dropzone
// ===========================================
KundenKarte.initFileDropzone = function() {
var dropzone = document.getElementById('fileDropzone');
var fileInput = document.getElementById('fileInput');
var selectedFilesDiv = document.getElementById('selectedFiles');
var uploadBtn = document.getElementById('uploadBtn');
var fileCountSpan = document.getElementById('fileCount');
var form = document.getElementById('fileUploadForm');
if (!dropzone || !fileInput) return;
var selectedFiles = [];
// Click to open file dialog
dropzone.addEventListener('click', function(e) {
if (e.target.tagName !== 'A') {
fileInput.click();
}
});
// Drag & Drop handlers
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
dropzone.addEventListener(eventName, function(e) {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(function(eventName) {
dropzone.addEventListener(eventName, function() {
dropzone.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(function(eventName) {
dropzone.addEventListener(eventName, function() {
dropzone.classList.remove('dragover');
});
});
dropzone.addEventListener('drop', function(e) {
var files = e.dataTransfer.files;
handleFiles(files);
});
fileInput.addEventListener('change', function() {
handleFiles(this.files);
});
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
// Check for duplicates
var isDuplicate = selectedFiles.some(function(f) {
return f.name === file.name && f.size === file.size;
});
if (!isDuplicate) {
selectedFiles.push(file);
}
}
updateFileList();
}
function updateFileList() {
selectedFilesDiv.innerHTML = '';
if (selectedFiles.length === 0) {
selectedFilesDiv.style.display = 'none';
uploadBtn.style.display = 'none';
return;
}
selectedFilesDiv.style.display = 'flex';
uploadBtn.style.display = 'inline-block';
fileCountSpan.textContent = selectedFiles.length;
selectedFiles.forEach(function(file, index) {
var fileDiv = document.createElement('div');
fileDiv.className = 'kundenkarte-dropzone-file';
var icon = getFileIcon(file.name);
fileDiv.innerHTML = ' ' +
'' + KundenKarte.AnlageConnection.escapeHtml(file.name) + '' +
'';
selectedFilesDiv.appendChild(fileDiv);
});
// Add remove handlers
selectedFilesDiv.querySelectorAll('.remove-file').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var idx = parseInt(this.getAttribute('data-index'));
selectedFiles.splice(idx, 1);
updateFileList();
});
});
// Update file input with DataTransfer
updateFileInput();
}
function updateFileInput() {
var dt = new DataTransfer();
selectedFiles.forEach(function(file) {
dt.items.add(file);
});
fileInput.files = dt.files;
}
function getFileIcon(filename) {
var ext = filename.split('.').pop().toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].indexOf(ext) !== -1) {
return 'fa-image';
} else if (ext === 'pdf') {
return 'fa-file-pdf-o';
} else if (['doc', 'docx'].indexOf(ext) !== -1) {
return 'fa-file-word-o';
} else if (['xls', 'xlsx'].indexOf(ext) !== -1) {
return 'fa-file-excel-o';
}
return 'fa-file-o';
}
};
// Auto-init on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', KundenKarte.initFileDropzone);
} else {
KundenKarte.initFileDropzone();
}
// ===========================================
// Field Autocomplete
// ===========================================
KundenKarte.FieldAutocomplete = {
baseUrl: '',
activeInput: null,
dropdown: null,
debounceTimer: null,
init: function(baseUrl) {
this.baseUrl = baseUrl || '';
this.createDropdown();
this.bindEvents();
},
createDropdown: function() {
// Create dropdown element if not exists
if (!document.getElementById('kk-autocomplete-dropdown')) {
var dropdown = document.createElement('div');
dropdown.id = 'kk-autocomplete-dropdown';
dropdown.className = 'kk-autocomplete-dropdown';
dropdown.style.cssText = 'display:none;position:absolute;z-index:10000;background:#2d2d2d;border:1px solid #555;border-radius:4px;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,0.3);';
document.body.appendChild(dropdown);
this.dropdown = dropdown;
} else {
this.dropdown = document.getElementById('kk-autocomplete-dropdown');
}
},
bindEvents: function() {
var self = this;
// Delegate events for autocomplete inputs
document.addEventListener('input', function(e) {
if (e.target.classList.contains('kk-autocomplete')) {
self.onInput(e.target);
}
});
document.addEventListener('focus', function(e) {
if (e.target.classList.contains('kk-autocomplete')) {
self.onFocus(e.target);
}
}, true);
document.addEventListener('blur', function(e) {
if (e.target.classList.contains('kk-autocomplete')) {
// Delay to allow click on dropdown item
setTimeout(function() {
self.hideDropdown();
}, 200);
}
}, true);
document.addEventListener('keydown', function(e) {
if (e.target.classList.contains('kk-autocomplete') && self.dropdown.style.display !== 'none') {
self.onKeydown(e);
}
});
// Click on dropdown item
this.dropdown.addEventListener('click', function(e) {
var item = e.target.closest('.kk-autocomplete-item');
if (item) {
self.selectItem(item);
}
});
},
onInput: function(input) {
var self = this;
this.activeInput = input;
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(function() {
self.fetchSuggestions(input);
}, 150);
},
onFocus: function(input) {
this.activeInput = input;
if (input.value.length >= 1) {
this.fetchSuggestions(input);
}
},
onKeydown: function(e) {
var items = this.dropdown.querySelectorAll('.kk-autocomplete-item');
var activeItem = this.dropdown.querySelector('.kk-autocomplete-item.active');
var activeIndex = -1;
items.forEach(function(item, idx) {
if (item.classList.contains('active')) activeIndex = idx;
});
if (e.key === 'ArrowDown') {
e.preventDefault();
if (activeIndex < items.length - 1) {
if (activeItem) activeItem.classList.remove('active');
items[activeIndex + 1].classList.add('active');
items[activeIndex + 1].scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (activeIndex > 0) {
if (activeItem) activeItem.classList.remove('active');
items[activeIndex - 1].classList.add('active');
items[activeIndex - 1].scrollIntoView({ block: 'nearest' });
}
} else if (e.key === 'Enter' && activeItem) {
e.preventDefault();
this.selectItem(activeItem);
} else if (e.key === 'Escape') {
this.hideDropdown();
}
},
fetchSuggestions: function(input) {
var self = this;
var fieldCode = input.getAttribute('data-field-code');
var typeId = input.getAttribute('data-type-id') || 0;
var query = input.value;
if (!fieldCode) return;
$.ajax({
url: this.baseUrl + '/custom/kundenkarte/ajax/field_autocomplete.php',
method: 'GET',
data: {
action: 'suggest',
field_code: fieldCode,
type_id: typeId,
query: query
},
dataType: 'json',
success: function(response) {
if (response.suggestions && response.suggestions.length > 0) {
self.showSuggestions(input, response.suggestions, query);
} else {
self.hideDropdown();
}
}
});
},
showSuggestions: function(input, suggestions, query) {
var self = this;
var rect = input.getBoundingClientRect();
this.dropdown.innerHTML = '';
this.dropdown.style.top = (rect.bottom + window.scrollY) + 'px';
this.dropdown.style.left = rect.left + 'px';
this.dropdown.style.width = rect.width + 'px';
this.dropdown.style.display = 'block';
suggestions.forEach(function(suggestion, idx) {
var item = document.createElement('div');
item.className = 'kk-autocomplete-item';
if (idx === 0) item.classList.add('active');
item.style.cssText = 'padding:8px 12px;cursor:pointer;color:#e0e0e0;border-bottom:1px solid #444;';
item.setAttribute('data-value', suggestion);
// Highlight matching text
var html = self.highlightMatch(suggestion, query);
item.innerHTML = html;
self.dropdown.appendChild(item);
});
// Hover effect
this.dropdown.querySelectorAll('.kk-autocomplete-item').forEach(function(item) {
item.addEventListener('mouseenter', function() {
self.dropdown.querySelectorAll('.kk-autocomplete-item').forEach(function(i) {
i.classList.remove('active');
});
item.classList.add('active');
});
});
},
highlightMatch: function(text, query) {
if (!query) return text;
var regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return text.replace(regex, '$1');
},
selectItem: function(item) {
var value = item.getAttribute('data-value');
if (this.activeInput) {
this.activeInput.value = value;
// Trigger change event
var event = new Event('change', { bubbles: true });
this.activeInput.dispatchEvent(event);
}
this.hideDropdown();
},
hideDropdown: function() {
if (this.dropdown) {
this.dropdown.style.display = 'none';
}
}
};
// Auto-init autocomplete
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
KundenKarte.FieldAutocomplete.init('');
});
} else {
KundenKarte.FieldAutocomplete.init('');
}
})();