';
@@ -6474,10 +6483,16 @@
html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1));
html += '';
- // TE markers on the rail
- for (var te = 0; te <= (carrier.total_te || 12); te++) {
+ // 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 += '
';
+ }
}
});
});
@@ -6506,9 +6521,15 @@
html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1));
html += '';
- for (var te = 0; te <= (carrier.total_te || 12); te++) {
+ // 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 += '
';
+ }
}
});
}
@@ -6538,7 +6559,7 @@
var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4;
var blockHeight = self.BLOCK_HEIGHT;
- var x = parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * self.TE_WIDTH + 2;
+ 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;
@@ -6666,7 +6687,7 @@
var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; });
var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; });
- var widthTE = parseInt(eq.width_te) || 1;
+ 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);
@@ -7406,35 +7427,34 @@
// Check if carrier has position data (use typeof to allow 0 values)
if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') {
- // Belegte Slots ermitteln (1-basiert)
- var totalTE = parseInt(carrier.total_te) || 12;
- var occupied = {};
+ // 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 = parseInt(eq.position_te) || 1;
- var w = parseInt(eq.width_te) || 1;
- var endPos = pos + w - 1;
- for (var s = pos; s <= endPos; s++) {
- occupied[s] = true;
- }
+ 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 currentGap = 0;
- for (var s = 1; s <= totalTE; s++) {
- if (!occupied[s]) {
- currentGap++;
- if (currentGap > maxGap) maxGap = currentGap;
- } else {
- currentGap = 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;
@@ -7868,7 +7888,7 @@
showAddBusbarDialog: function(carrierId) {
var self = this;
var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); });
- var totalTE = carrier ? (parseInt(carrier.total_te) || 12) : 12;
+ var totalTE = carrier ? (parseFloat(carrier.total_te) || 12) : 12;
var html = '
';
@@ -8051,7 +8071,7 @@
showEquipmentTypeSelector: function(carrierId, types, maxGap) {
var self = this;
- // Kategorisiere Equipment-Typen
+ // Kategorisiere Equipment-Typen (category kommt aus DB)
var categories = {
'schutz': { label: 'Schutzgeräte', icon: 'fa-shield', items: [] },
'automat': { label: 'Leitungsschutz', icon: 'fa-bolt', items: [] },
@@ -8062,18 +8082,12 @@
// Sortiere Typen in Kategorien (nur wenn Breite in verfügbare Lücke passt)
maxGap = maxGap || 99;
types.forEach(function(t) {
- var typeWidth = parseInt(t.width_te) || 1;
+ var typeWidth = parseFloat(t.width_te) || 1;
if (typeWidth > maxGap) return; // Passt nicht in verfügbare Lücke
- var ref = (t.ref || '').toUpperCase();
- var label = (t.label || '').toLowerCase();
-
- if (ref.indexOf('FI') === 0 || ref === 'AFDD' || ref.indexOf('SPD') === 0) {
- categories.schutz.items.push(t);
- } else if (ref.indexOf('LS') === 0 || ref.indexOf('HS') === 0 || ref.indexOf('NH') === 0 || ref === 'MSS') {
- categories.automat.items.push(t);
- } else if (ref.indexOf('RK_') === 0 || label.indexOf('klemme') !== -1) {
- categories.klemme.items.push(t);
+ var cat = t.category || 'steuerung';
+ if (categories[cat]) {
+ categories[cat].items.push(t);
} else {
categories.steuerung.items.push(t);
}
@@ -8365,9 +8379,9 @@
// Returns: { top: boolean, bottom: boolean } indicating which terminals are covered
isEquipmentCoveredByBusbar: function(eq) {
var eqCarrierId = eq.carrier_id || eq.fk_carrier;
- var eqPosTE = parseInt(eq.position_te) || 1;
- var eqWidthTE = parseInt(eq.width_te) || 1;
- var eqEndTE = eqPosTE + eqWidthTE - 1;
+ var eqPosTE = parseFloat(eq.position_te) || 1;
+ var eqWidthTE = parseFloat(eq.width_te) || 1;
+ var eqEndTE = eqPosTE + eqWidthTE; // Half-open range: [eqPosTE, eqEndTE)
var result = { top: false, bottom: false };
@@ -8383,8 +8397,8 @@
var railEnd = parseInt(conn.rail_end_te) || railStart;
var positionY = parseInt(conn.position_y) || 0;
- // Check if equipment overlaps with busbar range
- var overlaps = !(eqEndTE < railStart || eqPosTE > railEnd);
+ // Overlap: [eqPosTE, eqEndTE) vs [railStart, railEnd+1)
+ var overlaps = eqPosTE < railEnd + 1 && railStart < eqEndTE;
if (!overlaps) return;
// Determine if busbar is above (position_y = 0) or below (position_y > 0)
@@ -8408,7 +8422,9 @@
// Try to parse terminals_config from equipment type
if (eq.terminals_config) {
try {
- var config = JSON.parse(eq.terminals_config);
+ // Literale \r\n bereinigen (falls DB-Daten kaputt)
+ var configStr = eq.terminals_config.replace(/\\r\\n|\\r|\\n/g, ' ').replace(/\\t/g, '');
+ var config = JSON.parse(configStr);
// Handle both old format (inputs/outputs) and new format (terminals)
if (config.terminals) {
return config.terminals;
@@ -8501,7 +8517,7 @@
}
var isTop = terminal.pos === 'top';
- var widthTE = parseInt(eq.width_te) || 1;
+ var widthTE = parseFloat(eq.width_te) || 1;
// Terminal im festen TE-Raster platzieren
// Jeder Terminal belegt 1 TE - Index bestimmt welches TE
@@ -8577,7 +8593,7 @@
this.equipment.forEach(function(eq) {
if (eq._x === undefined || eq._y === undefined) return;
- var eqWidth = eq._width || (self.TE_WIDTH * (parseInt(eq.width_te) || 1));
+ var eqWidth = eq._width || (self.TE_WIDTH * (parseFloat(eq.width_te) || 1));
var eqHeight = eq._height || self.BLOCK_HEIGHT;
var eqX1 = Math.floor((eq._x - minX) / GRID_SIZE);
@@ -10456,21 +10472,26 @@
var carrier = this.carriers.find(function(c) { return parseInt(c.id) === parseInt(carrierId); });
var carrierLabel = carrier ? (carrier.label || 'Hutschiene') : 'Hutschiene';
- this.showConfirmDialog('Hutschiene löschen', 'Hutschiene "' + carrierLabel + '" wirklich löschen? Alle darauf platzierten Geräte werden ebenfalls gelöscht!', function() {
+ KundenKarte.showConfirm('Hutschiene löschen', 'Hutschiene "' + carrierLabel + '" wirklich löschen? Alle darauf platzierten Geräte werden ebenfalls gelöscht!', function() {
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php',
method: 'POST',
data: {
action: 'delete',
- carrier_id: carrierId
+ carrier_id: carrierId,
+ token: KundenKarte.token
},
dataType: 'json',
success: function(response) {
if (response.success) {
- self.loadData(); // Reload all data
+ self.loadData();
+ self.showMessage('Hutschiene gelöscht', 'success');
} else {
- alert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
+ self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error');
}
+ },
+ error: function() {
+ self.showMessage('Fehler beim Löschen', 'error');
}
});
});
@@ -10535,7 +10556,7 @@
// Position
dialogHtml += '
';
dialogHtml += '';
- dialogHtml += '
';
// Produktauswahl mit Autocomplete
@@ -10700,8 +10721,8 @@
element: $block,
startX: e.pageX,
startY: e.pageY,
- originalTE: parseInt(eq.position_te) || 1,
- originalX: parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2,
+ originalTE: parseFloat(eq.position_te) || 1,
+ originalX: parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2,
originalY: eq._y
};
@@ -10724,13 +10745,14 @@
var targetCarrier = this.findClosestCarrier(mouseY, mouseX);
if (!targetCarrier) targetCarrier = this.dragState.carrier;
- // Calculate TE position on target carrier based on absolute X position
+ // TE-Position auf Ziel-Carrier berechnen (0.1er-Snap für Dezimal-Breiten)
var relativeX = mouseX - targetCarrier._x;
- var newTE = Math.round(relativeX / this.TE_WIDTH) + 1;
+ var rawTE = relativeX / this.TE_WIDTH + 1;
+ var newTE = Math.round(rawTE * 10) / 10;
- // Clamp to valid range on TARGET carrier
- var maxTE = (parseInt(targetCarrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1;
- newTE = Math.max(1, Math.min(newTE, maxTE));
+ // 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;
@@ -10962,11 +10984,7 @@
showMessage: function(text, type) {
var $msg = $('.schematic-message');
- if (!$msg.length) {
- // Create permanent status bar with fixed height
- $msg = $('
');
- $('.schematic-editor-canvas').prepend($msg);
- }
+ if (!$msg.length) return; // Statuszeile wird in PHP erstellt
// Clear any pending hide timeout
if (this.messageTimeout) {
@@ -10974,20 +10992,19 @@
this.messageTimeout = null;
}
- $msg.removeClass('success error warning info').addClass(type).text(text).css('opacity', 1);
+ // 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() {
- // Don't hide, just clear the message text but keep the bar
- $msg.removeClass('success error warning info').addClass('info').text('Bereit');
+ $msg.attr('class', 'schematic-message info').text('Bereit');
}, 2500);
}
},
hideMessage: function() {
var $msg = $('.schematic-message');
- // Don't hide, just reset to default state
- $msg.removeClass('success error warning info').addClass('info').text('Bereit');
+ $msg.attr('class', 'schematic-message info').text('Bereit');
},
escapeHtml: function(text) {
diff --git a/js/pwa.js b/js/pwa.js
index 77f6883..a62233c 100644
--- a/js/pwa.js
+++ b/js/pwa.js
@@ -709,7 +709,7 @@
carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0));
const totalTe = parseInt(carrier.total_te) || 12;
- const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseInt(eq.width_te) || 1), 0);
+ const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseFloat(eq.width_te) || 1), 0);
const isFull = usedTe >= totalTe;
html += `
@@ -723,8 +723,8 @@
// === Zeile 1: Terminals oben (Inputs + Top-Outputs) ===
carrierEquipment.forEach(eq => {
- const widthTe = parseInt(eq.width_te) || 1;
- const posTe = parseInt(eq.position_te) || 0;
+ const widthTe = parseFloat(eq.width_te) || 1;
+ const posTe = parseFloat(eq.position_te) || 0;
const eqInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id) : [];
const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : [];
@@ -757,8 +757,8 @@
// === Zeile 2: Equipment-Blöcke ===
carrierEquipment.forEach(eq => {
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
- const widthTe = parseInt(eq.width_te) || 1;
- const posTe = parseInt(eq.position_te) || 0;
+ const widthTe = parseFloat(eq.width_te) || 1;
+ const posTe = parseFloat(eq.position_te) || 0;
const typeLabel = type?.label_short || type?.ref || '';
const blockColor = eq.block_color || type?.color || '#3498db';
@@ -788,8 +788,8 @@
// === Zeile 3: Output-Terminals unten (Standard-Abgänge) ===
carrierEquipment.forEach(eq => {
- const widthTe = parseInt(eq.width_te) || 1;
- const posTe = parseInt(eq.position_te) || 0;
+ const widthTe = parseFloat(eq.width_te) || 1;
+ const posTe = parseFloat(eq.position_te) || 0;
const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : [];
for (let t = 0; t < widthTe; t++) {
@@ -1074,26 +1074,24 @@
const totalTe = parseInt(carrier.total_te) || 12;
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId);
- // Belegte Slots ermitteln (1-basiert)
- const occupied = {};
- carrierEquipment.forEach(eq => {
- const pos = parseInt(eq.position_te) || 1;
- const w = parseInt(eq.width_te) || 1;
- for (let s = pos; s < pos + w; s++) {
- occupied[s] = true;
- }
- });
+ // Belegte Ranges ermitteln (1-basiert, Dezimal-Breiten)
+ const ranges = carrierEquipment.map(eq => ({
+ start: parseFloat(eq.position_te) || 1,
+ end: (parseFloat(eq.position_te) || 1) + (parseFloat(eq.width_te) || 1)
+ })).sort((a, b) => a.start - b.start);
// Maximale zusammenhängende Lücke
- let maxGap = 0, currentGap = 0;
- for (let s = 1; s <= totalTe; s++) {
- if (!occupied[s]) {
- currentGap++;
- if (currentGap > maxGap) maxGap = currentGap;
- } else {
- currentGap = 0;
- }
+ let maxGap = 0;
+ let pos = 1;
+ const railEnd = totalTe + 1;
+ for (const range of ranges) {
+ const gap = range.start - pos;
+ if (gap > maxGap) maxGap = gap;
+ if (range.end > pos) pos = range.end;
}
+ // Lücke nach letztem Element
+ const endGap = railEnd - pos;
+ if (endGap > maxGap) maxGap = endGap;
return maxGap;
}
@@ -1304,26 +1302,27 @@
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId);
const carrier = App.carriers.find(c => c.id == App.currentCarrierId);
const totalTe = parseInt(carrier?.total_te) || 12;
- const eqWidth = parseInt(type?.width_te) || 1;
+ const eqWidth = parseFloat(type?.width_te) || 1;
- // Belegungsarray erstellen
- const occupied = new Array(totalTe + 1).fill(false);
- carrierEquipment.forEach(e => {
- const pos = parseInt(e.position_te) || 1;
- const w = parseInt(e.width_te) || 1;
- for (let i = pos; i < pos + w && i <= totalTe; i++) {
- occupied[i] = true;
- }
- });
+ // Belegte Ranges ermitteln (Dezimal-TE-Unterstützung)
+ const ranges = carrierEquipment.map(e => ({
+ start: parseFloat(e.position_te) || 1,
+ end: (parseFloat(e.position_te) || 1) + (parseFloat(e.width_te) || 1)
+ })).sort((a, b) => a.start - b.start);
// Erste Lücke finden die breit genug ist
let nextPos = 0;
- for (let i = 1; i <= totalTe - eqWidth + 1; i++) {
- let fits = true;
- for (let j = 0; j < eqWidth; j++) {
- if (occupied[i + j]) { fits = false; break; }
+ let pos = 1;
+ const railEnd = totalTe + 1;
+ for (const range of ranges) {
+ if (pos + eqWidth <= range.start + 0.001) {
+ nextPos = pos;
+ break;
}
- if (fits) { nextPos = i; break; }
+ if (range.end > pos) pos = range.end;
+ }
+ if (nextPos === 0 && pos + eqWidth <= railEnd + 0.001) {
+ nextPos = pos;
}
if (nextPos === 0) {
diff --git a/tabs/anlagen.php b/tabs/anlagen.php
index fce9bda..d463f22 100755
--- a/tabs/anlagen.php
+++ b/tabs/anlagen.php
@@ -745,8 +745,8 @@ if (empty($customerSystems)) {
print '';
print '