diff --git a/ChangeLog.md b/ChangeLog.md index 425c3b7..da082c9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,6 +14,30 @@ - Leitungen "verschwinden" hinter Bloecken und kommen auf der anderen Seite wieder raus - Professionelleres Erscheinungsbild wie in echten Schaltplan-Editoren +- **Wire-Segment-Dragging**: Leitungen koennen verschoben werden ohne Verbindungen zu verlieren + - Shift+Klick oder Mittlere Maustaste auf Leitungssegment zum Ziehen + - Horizontale Segmente nur vertikal verschiebbar, vertikale nur horizontal + - Start- und End-Segmente (an Terminals) bleiben fix + - Automatisches Grid-Snapping (25px) + - Live-Vorschau waehrend dem Ziehen + - Neue Funktionen: `parsePathToPoints()`, `pointsToPath()`, `findClickedSegment()` + - `startWireDrag()`, `handleWireDragMove()`, `finishWireDrag()`, `cancelWireDrag()` + +- **Busbar-Typen aus Datenbank**: Phasenschienen-Typen dynamisch aus DB laden + - Edit-Dialog nutzt jetzt `busbarTypes` Array statt hardcodierter Optionen + - `fk_busbar_type` wird beim Update korrekt gespeichert + - Admin-Seite fuer Busbar-Typen mit phases_config JSON-Feld + +- **PWA: Farbpropagierung bei Einspeisung**: Automatische Farbuebernahme + - Bei Auswahl einer Input-Phase (L1/L2/L3/N/PE) wird Farbe automatisch gesetzt + - Funktion `propagateInputColor()` aktualisiert Farben auf Abgaengen + - Phase-Matching: L1 matched L1, LN, L1N; N matched N; etc. + - Funktioniert online und offline (mit Queue) + +- **PWA: N-Phase als Einspeisung**: Neutralleiter jetzt als Input-Phase waehlbar + - INPUT_PHASES erweitert um 'N': ['L1', 'L2', 'L3', 'N', 'PE'] + - 3P und 3P+N entfernt (nur Einzel-Phasen) + ### Verbesserungen - **Zeichenmodus-Verhalten**: Konsistentes Verhalten im manuellen Zeichenmodus diff --git a/js/kundenkarte.js b/js/kundenkarte.js index cde2100..b7faf68 100644 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -5222,6 +5222,14 @@ wireExtendExistingPoints: [], // Existing path points of connection being extended WIRE_GRID_SIZE: 25, // Snap grid size in pixels (larger = fewer points) + // Wire segment dragging + wireDragMode: false, // Currently dragging a wire segment + wireDragConnId: null, // Connection ID being dragged + wireDragSegmentIndex: null, // Index of segment being dragged (0-based) + wireDragIsHorizontal: null, // true = horizontal segment (drag Y), false = vertical (drag X) + wireDragStartPos: null, // Starting mouse position + wireDragOriginalPoints: [], // Original path points before drag + // Display settings (persisted in localStorage) displaySettings: { phaseColors: true, // Color wires by phase (L1=brown, L2=black, L3=gray, N=blue, PE=green) @@ -8031,6 +8039,44 @@ this.addEventListener('mouseleave', function() { $visiblePath.attr('stroke-width', wireWidth); }); + + // Wire segment dragging - mousedown to start drag + this.addEventListener('mousedown', function(e) { + // Only with middle mouse button or Shift+left click + if (e.button === 1 || (e.button === 0 && e.shiftKey)) { + e.preventDefault(); + e.stopPropagation(); + + // Don't start drag if in wire draw mode + if (self.wireDrawMode) return; + + var $svg = $(e.target).closest('svg'); + var svgRect = $svg[0].getBoundingClientRect(); + var clickX = e.clientX - svgRect.left; + var clickY = e.clientY - svgRect.top; + + if (self.startWireDrag(connId, clickX, clickY)) { + // Successfully started drag - bind move/up handlers + var moveHandler = function(moveE) { + var mx = moveE.clientX - svgRect.left; + var my = moveE.clientY - svgRect.top; + self.handleWireDragMove(mx, my); + }; + + var upHandler = function(upE) { + var ux = upE.clientX - svgRect.left; + var uy = upE.clientY - svgRect.top; + self.finishWireDrag(ux, uy); + document.removeEventListener('mousemove', moveHandler); + document.removeEventListener('mouseup', upHandler); + }; + + document.addEventListener('mousemove', moveHandler); + document.addEventListener('mouseup', upHandler); + } + } + }); + this.style.cursor = 'pointer'; }); @@ -8193,6 +8239,37 @@ this.addEventListener('mouseleave', function() { $visiblePath.attr('stroke-width', wireWidth); }); + + // Wire segment dragging + this.addEventListener('mousedown', function(e) { + if (e.button === 1 || (e.button === 0 && e.shiftKey)) { + e.preventDefault(); + e.stopPropagation(); + if (self.wireDrawMode) return; + + var $svg = $(e.target).closest('svg'); + var svgRect = $svg[0].getBoundingClientRect(); + var clickX = e.clientX - svgRect.left; + var clickY = e.clientY - svgRect.top; + + if (self.startWireDrag(connId, clickX, clickY)) { + var moveHandler = function(moveE) { + var mx = moveE.clientX - svgRect.left; + var my = moveE.clientY - svgRect.top; + self.handleWireDragMove(mx, my); + }; + var upHandler = function(upE) { + var ux = upE.clientX - svgRect.left; + var uy = upE.clientY - svgRect.top; + self.finishWireDrag(ux, uy); + document.removeEventListener('mousemove', moveHandler); + document.removeEventListener('mouseup', upHandler); + }; + document.addEventListener('mousemove', moveHandler); + document.addEventListener('mouseup', upHandler); + } + } + }); this.style.cursor = 'pointer'; }); }, @@ -10802,6 +10879,226 @@ } }, + // ======================================== + // Wire Segment Dragging + // ======================================== + + /** + * Parse SVG path data to array of points + */ + parsePathToPoints: function(pathData) { + if (!pathData) return []; + var points = []; + var regex = /([ML])\s*([\d.]+)\s+([\d.]+)/g; + var match; + while ((match = regex.exec(pathData)) !== null) { + points.push({ + x: parseFloat(match[2]), + y: parseFloat(match[3]) + }); + } + return points; + }, + + /** + * Convert points array back to SVG path string + */ + pointsToPath: function(points) { + if (!points || points.length === 0) return ''; + var path = ''; + for (var i = 0; i < points.length; i++) { + path += (i === 0 ? 'M ' : 'L ') + Math.round(points[i].x) + ' ' + Math.round(points[i].y) + ' '; + } + return path.trim(); + }, + + /** + * Find which segment of a path was clicked + * Returns: { index: segmentIndex, isHorizontal: bool } or null + */ + findClickedSegment: function(points, clickX, clickY, threshold) { + threshold = threshold || 15; + + for (var i = 0; i < points.length - 1; i++) { + var p1 = points[i]; + var p2 = points[i + 1]; + + // Check if click is near this segment + var isHorizontal = Math.abs(p1.y - p2.y) < 2; // Horizontal if Y values are same + var isVertical = Math.abs(p1.x - p2.x) < 2; // Vertical if X values are same + + if (isHorizontal) { + // Horizontal segment - check if click is within X range and near Y + var minX = Math.min(p1.x, p2.x); + var maxX = Math.max(p1.x, p2.x); + if (clickX >= minX - threshold && clickX <= maxX + threshold && + Math.abs(clickY - p1.y) <= threshold) { + return { index: i, isHorizontal: true }; + } + } else if (isVertical) { + // Vertical segment - check if click is within Y range and near X + var minY = Math.min(p1.y, p2.y); + var maxY = Math.max(p1.y, p2.y); + if (clickY >= minY - threshold && clickY <= maxY + threshold && + Math.abs(clickX - p1.x) <= threshold) { + return { index: i, isHorizontal: false }; + } + } + } + return null; + }, + + /** + * Start dragging a wire segment + */ + startWireDrag: function(connId, clickX, clickY) { + var conn = this.connections.find(function(c) { return c.id == connId; }); + if (!conn || !conn.path_data) return false; + + var points = this.parsePathToPoints(conn.path_data); + if (points.length < 3) return false; // Need at least 3 points to have a draggable segment + + var segment = this.findClickedSegment(points, clickX, clickY); + if (!segment) return false; + + // Don't allow dragging the first or last segment (connected to terminals) + if (segment.index === 0 || segment.index === points.length - 2) { + this.showMessage('Start- und End-Segmente können nicht verschoben werden', 'warning'); + return false; + } + + this.wireDragMode = true; + this.wireDragConnId = connId; + this.wireDragSegmentIndex = segment.index; + this.wireDragIsHorizontal = segment.isHorizontal; + this.wireDragStartPos = { x: clickX, y: clickY }; + this.wireDragOriginalPoints = JSON.parse(JSON.stringify(points)); + + // Visual feedback + $(this.svgElement).css('cursor', segment.isHorizontal ? 'ns-resize' : 'ew-resize'); + this.log('Wire drag started - segment:', segment.index, 'horizontal:', segment.isHorizontal); + + return true; + }, + + /** + * Handle mouse move during wire drag + */ + handleWireDragMove: function(mouseX, mouseY) { + if (!this.wireDragMode) return; + + var points = JSON.parse(JSON.stringify(this.wireDragOriginalPoints)); + var segIdx = this.wireDragSegmentIndex; + + if (this.wireDragIsHorizontal) { + // Horizontal segment - move Y (vertical drag) + var deltaY = mouseY - this.wireDragStartPos.y; + var newY = points[segIdx].y + deltaY; + + // Snap to grid + newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + + // Update both points of this segment + points[segIdx].y = newY; + points[segIdx + 1].y = newY; + } else { + // Vertical segment - move X (horizontal drag) + var deltaX = mouseX - this.wireDragStartPos.x; + var newX = points[segIdx].x + deltaX; + + // Snap to grid + newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + + // Update both points of this segment + points[segIdx].x = newX; + points[segIdx + 1].x = newX; + } + + // Update the visual path in SVG + var newPath = this.pointsToPath(points); + var $conn = $(this.svgElement).find('.schematic-connection[data-connection-id="' + this.wireDragConnId + '"]'); + var $hitarea = $(this.svgElement).find('.schematic-connection-hitarea[data-connection-id="' + this.wireDragConnId + '"]'); + var $shadow = $conn.closest('.schematic-connection-group').find('.schematic-connection-shadow'); + + $conn.attr('d', newPath); + $hitarea.attr('d', newPath); + $shadow.attr('d', newPath); + }, + + /** + * Finish wire drag and save + */ + finishWireDrag: function(mouseX, mouseY) { + if (!this.wireDragMode) return; + + var points = JSON.parse(JSON.stringify(this.wireDragOriginalPoints)); + var segIdx = this.wireDragSegmentIndex; + + if (this.wireDragIsHorizontal) { + var deltaY = mouseY - this.wireDragStartPos.y; + var newY = points[segIdx].y + deltaY; + newY = Math.round(newY / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + points[segIdx].y = newY; + points[segIdx + 1].y = newY; + } else { + var deltaX = mouseX - this.wireDragStartPos.x; + var newX = points[segIdx].x + deltaX; + newX = Math.round(newX / this.WIRE_GRID_SIZE) * this.WIRE_GRID_SIZE; + points[segIdx].x = newX; + points[segIdx + 1].x = newX; + } + + // Clean up collinear points + points = this.cleanupPathPoints(points); + + var newPath = this.pointsToPath(points); + var self = this; + + // Save to server + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'update', + connection_id: this.wireDragConnId, + path_data: newPath, + token: $('input[name="token"]').val() + }, + success: function(response) { + if (response.success) { + // Update local data + var conn = self.connections.find(function(c) { return c.id == self.wireDragConnId; }); + if (conn) { + conn.path_data = newPath; + } + self.showMessage('Leitung verschoben', 'success'); + } else { + self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error'); + self.renderConnections(); // Revert visual + } + self.cancelWireDrag(); + }, + error: function() { + self.showMessage('Netzwerkfehler', 'error'); + self.renderConnections(); // Revert visual + self.cancelWireDrag(); + } + }); + }, + + /** + * Cancel wire drag + */ + cancelWireDrag: function() { + this.wireDragMode = false; + this.wireDragConnId = null; + this.wireDragSegmentIndex = null; + this.wireDragIsHorizontal = null; + this.wireDragStartPos = null; + this.wireDragOriginalPoints = []; + $(this.svgElement).css('cursor', ''); + }, + // Update an existing connection with extended path updateExtendedConnection: function(connId, newTargetEqId, newTargetTermId, newPathData) { var self = this; diff --git a/js/pwa.js b/js/pwa.js index f173ed6..a625a59 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -226,6 +226,17 @@ // Medium-Type Change -> Spezifikationen laden $('#conn-medium-type').on('change', handleMediumTypeChange); + // Connection-Type Change -> Auto-Farbe bei Input-Phasen + $('#conn-type').on('change', function() { + if (App.connectionDirection === 'input') { + const phase = $(this).val(); + if (phase) { + const color = getPhaseColor(phase); + $('#conn-color').val(color); + } + } + }); + // Bestätigungsdialog $('#btn-confirm-ok').on('click', function() { closeModal('confirm'); @@ -2790,7 +2801,13 @@ const response = await apiCall('ajax/pwa_api.php', data); if (response.success) { const list = App.connectionDirection === 'input' ? App.inputs : App.outputs; - updateLocal(list.find(c => c.id == App.editConnectionId)); + const conn = list.find(c => c.id == App.editConnectionId); + updateLocal(conn); + // Farbpropagierung bei Input + if (App.connectionDirection === 'input' && connectionType) { + const eqId = conn ? conn.fk_target : null; + await propagateInputColor(eqId, connectionType, color); + } renderEditor(); showToast('Verbindung aktualisiert', 'success'); } else { @@ -2803,7 +2820,13 @@ } else { queueOfflineAction(data); const list = App.connectionDirection === 'input' ? App.inputs : App.outputs; - updateLocal(list.find(c => c.id == App.editConnectionId)); + const conn = list.find(c => c.id == App.editConnectionId); + updateLocal(conn); + // Farbpropagierung bei Input (offline) + if (App.connectionDirection === 'input' && connectionType) { + const eqId = conn ? conn.fk_target : null; + propagateInputColor(eqId, connectionType, color); + } renderEditor(); showToast('Wird synchronisiert...', 'warning'); } @@ -2846,6 +2869,10 @@ if (App.connectionDirection === 'input') { newConn.fk_target = App.connectionEquipmentId; App.inputs.push(newConn); + // Farbpropagierung bei neuem Input + if (connectionType) { + await propagateInputColor(App.connectionEquipmentId, connectionType, color); + } } else { newConn.fk_source = App.connectionEquipmentId; App.outputs.push(newConn); @@ -2865,6 +2892,10 @@ if (App.connectionDirection === 'input') { newConn.fk_target = App.connectionEquipmentId; App.inputs.push(newConn); + // Farbpropagierung bei neuem Input (offline) + if (connectionType) { + propagateInputColor(App.connectionEquipmentId, connectionType, color); + } } else { newConn.fk_source = App.connectionEquipmentId; App.outputs.push(newConn); @@ -2877,6 +2908,60 @@ App.editConnectionId = null; } + /** + * Farbpropagierung: Wenn Input-Phase gesetzt, Farbe auf Outputs übertragen + * @param {number} equipmentId - Equipment-ID + * @param {string} phase - Phase (L1, L2, L3, N, PE) + * @param {string} color - Farbe der Einspeisung + */ + async function propagateInputColor(equipmentId, phase, color) { + if (!equipmentId || !phase || !color) return; + + // Finde alle Outputs dieses Equipment + const outputs = App.outputs.filter(o => o.fk_source == equipmentId); + if (outputs.length === 0) return; + + // Update Outputs mit passender Phase + for (const output of outputs) { + // Phase-Matching: L1 -> L1, LN, L1N; L2 -> L2; etc. + const outputType = output.connection_type || ''; + let matches = false; + + if (phase === 'L1' && (outputType.includes('L1') || outputType === 'LN')) matches = true; + else if (phase === 'L2' && outputType.includes('L2')) matches = true; + else if (phase === 'L3' && outputType.includes('L3')) matches = true; + else if (phase === 'N' && outputType.includes('N')) matches = true; + else if (phase === 'PE' && outputType.includes('PE')) matches = true; + + if (matches && output.color !== color) { + output.color = color; + + // Backend aktualisieren + if (App.isOnline) { + try { + await apiCall('ajax/pwa_api.php', { + action: 'update_connection', + connection_id: output.id, + color: color + }); + } catch (err) { + queueOfflineAction({ + action: 'update_connection', + connection_id: output.id, + color: color + }); + } + } else { + queueOfflineAction({ + action: 'update_connection', + connection_id: output.id, + color: color + }); + } + } + } + } + /** * Connection löschen (mit Bestätigung) */ diff --git a/sw.js b/sw.js index 63b9e88..54cb922 100644 --- a/sw.js +++ b/sw.js @@ -3,8 +3,8 @@ * Offline-First für Schaltschrank-Dokumentation */ -const CACHE_NAME = 'kundenkarte-pwa-v11.6'; -const OFFLINE_CACHE = 'kundenkarte-offline-v11.6'; +const CACHE_NAME = 'kundenkarte-pwa-v11.7'; +const OFFLINE_CACHE = 'kundenkarte-offline-v11.7'; // Statische Assets die immer gecached werden (ohne Query-String) const STATIC_ASSETS = [