Compare commits

..

2 commits

Author SHA1 Message Date
2995b8fa4e Version 3.1 - Schaltplan-Editor Verbesserungen
- Hutschiene per Klick bearbeitbar (Edit/Delete Popup)
- Terminal-Farben vereinheitlicht (grau)
- Debug-Code entfernt
- Dokumentation aktualisiert

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 17:41:53 +01:00
49087bf8f6 Fehler beseitigt in der Anzeige 2026-02-11 17:41:05 +01:00
4 changed files with 247 additions and 37 deletions

View file

@ -17,6 +17,16 @@ Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden
- Datei-Upload mit Bild-Vorschau und PDF-Anzeige - Datei-Upload mit Bild-Vorschau und PDF-Anzeige
- Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude) - Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude)
### Verteilungsdokumentation (Schaltplan-Editor)
- Interaktiver SVG-basierter Schaltplan-Editor
- Felder (Panels) und Hutschienen visuell verwalten
- Equipment-Bloecke per Drag & Drop positionieren
- Sammelschienen (Busbars) fuer Phasenverteilung
- Verbindungen zwischen Geraeten zeichnen
- Abgaenge und Anschlusspunkte dokumentieren
- Klickbare Hutschienen zum Bearbeiten
- Zoom und Pan fuer grosse Schaltplaene
### PDF Export ### PDF Export
- Export der Anlagenstruktur als PDF - Export der Anlagenstruktur als PDF
- Upload einer PDF-Vorlage als Briefpapier/Hintergrund - Upload einer PDF-Vorlage als Briefpapier/Hintergrund
@ -46,6 +56,7 @@ Im Admin-Bereich (Home > Setup > Module > KundenKarte) koennen Sie:
- **Anlagen-Systeme**: System-Kategorien anlegen (z.B. Strom, Internet) - **Anlagen-Systeme**: System-Kategorien anlegen (z.B. Strom, Internet)
- **Element-Typen**: Geraetetypen definieren (z.B. Zaehler, Router, Wallbox) - **Element-Typen**: Geraetetypen definieren (z.B. Zaehler, Router, Wallbox)
- **Typ-Felder**: Individuelle Felder pro Geraetetyp konfigurieren - **Typ-Felder**: Individuelle Felder pro Geraetetyp konfigurieren
- **Equipment-Typen**: Schaltplan-Komponenten (z.B. Sicherungsautomaten, FI-Schalter) mit Breite (TE), Farbe und Terminal-Konfiguration
## Berechtigungen ## Berechtigungen

View file

@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte' $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.0'; $this->version = '3.1';
// Url to the file with your last numberversion of this module // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -1652,7 +1652,12 @@
/* Rails (Hutschienen) */ /* Rails (Hutschienen) */
.schematic-rail { .schematic-rail {
cursor: default !important; cursor: pointer !important;
transition: filter 0.2s ease !important;
}
.schematic-rail:hover {
filter: brightness(1.3) !important;
} }
.schematic-rail-bg { .schematic-rail-bg {

View file

@ -4465,7 +4465,10 @@
}, },
init: function(anlageId) { init: function(anlageId) {
if (!anlageId) return; if (!anlageId) {
console.error('SchematicEditor.init: No anlageId provided!');
return;
}
this.anlageId = anlageId; this.anlageId = anlageId;
this.bindEvents(); this.bindEvents();
this.loadData(); this.loadData();
@ -4528,7 +4531,7 @@
// Hide popups when clicking elsewhere // Hide popups when clicking elsewhere
$(document).off('mousedown.hidePopup').on('mousedown.hidePopup', function(e) { $(document).off('mousedown.hidePopup').on('mousedown.hidePopup', function(e) {
// Don't hide if clicking on popup buttons // Don't hide if clicking on popup buttons
if ($(e.target).closest('.schematic-connection-popup, .schematic-equipment-popup').length) { if ($(e.target).closest('.schematic-connection-popup, .schematic-equipment-popup, .schematic-carrier-popup').length) {
return; return;
} }
// Don't hide if clicking on SVG elements (handlers will handle it) // Don't hide if clicking on SVG elements (handlers will handle it)
@ -4537,6 +4540,7 @@
} }
self.hideConnectionPopup(); self.hideConnectionPopup();
self.hideEquipmentPopup(); self.hideEquipmentPopup();
self.hideCarrierPopup();
}); });
// Clear all connections // Clear all connections
@ -4584,12 +4588,14 @@
self.showBusbarPopup(connectionId, e.clientX, e.clientY); self.showBusbarPopup(connectionId, e.clientX, e.clientY);
}); });
// Hutschiene (rail) click - show edit popup // Rail (Hutschiene) click - show popup for editing
$(document).off('click.rail').on('click.rail', '.schematic-rail', function(e) { $(document).off('click.rail').on('click.rail', '.schematic-rail', function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var carrierId = $(this).data('carrier-id'); var carrierId = $(this).data('carrier-id');
if (carrierId) {
self.showCarrierPopup(carrierId, e.clientX, e.clientY); self.showCarrierPopup(carrierId, e.clientX, e.clientY);
}
}); });
// Terminal right-click - show output/cable dialog // Terminal right-click - show output/cable dialog
@ -4989,11 +4995,9 @@
// Prevent multiple simultaneous loads (race condition fix) // Prevent multiple simultaneous loads (race condition fix)
if (this.isLoading) { if (this.isLoading) {
console.log('SchematicEditor loadData: Already loading, skipping...');
return; return;
} }
this.isLoading = true; this.isLoading = true;
console.log('SchematicEditor loadData: Starting...');
// Load panels with carriers for this anlage // Load panels with carriers for this anlage
$.ajax({ $.ajax({
@ -5003,7 +5007,6 @@
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
self.panels = response.panels || []; self.panels = response.panels || [];
console.log('SchematicEditor loadData: Loaded ' + self.panels.length + ' panels');
// Flatten carriers from all panels // Flatten carriers from all panels
self.carriers = []; self.carriers = [];
self.panels.forEach(function(panel) { self.panels.forEach(function(panel) {
@ -5014,16 +5017,15 @@
}); });
} }
}); });
console.log('SchematicEditor loadData: Loaded ' + self.carriers.length + ' carriers');
self.loadEquipment(); self.loadEquipment();
} else { } else {
console.error('SchematicEditor loadData: Failed to load panels', response);
self.isLoading = false; self.isLoading = false;
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
console.error('SchematicEditor loadData: AJAX error', error);
self.isLoading = false; self.isLoading = false;
// Show visible error
$('.schematic-editor-canvas').html('<div style="padding:20px;color:#e74c3c;">AJAX Fehler beim Laden: ' + error + '</div>');
} }
}); });
}, },
@ -5054,7 +5056,6 @@
$.when.apply($, promises).then(function() { $.when.apply($, promises).then(function() {
self.loadConnections(); self.loadConnections();
}).fail(function() { }).fail(function() {
console.error('SchematicEditor loadEquipment: One or more AJAX requests failed');
self.isLoading = false; self.isLoading = false;
}); });
}, },
@ -5079,10 +5080,6 @@
}); });
} }
console.log('SchematicEditor: Loaded ' + self.connections.length + ' connections', self.connections);
console.log('SchematicEditor: Equipment count: ' + self.equipment.length, self.equipment);
} else {
console.error('SchematicEditor: Failed to load connections', response);
} }
// Initialize canvas now that all data is loaded // Initialize canvas now that all data is loaded
if (!self.isInitialized) { if (!self.isInitialized) {
@ -5093,8 +5090,7 @@
// Reset loading flag // Reset loading flag
self.isLoading = false; self.isLoading = false;
}, },
error: function(xhr, status, error) { error: function() {
console.error('SchematicEditor: AJAX error loading connections', error);
self.isLoading = false; self.isLoading = false;
} }
}); });
@ -5161,7 +5157,12 @@
initCanvas: function() { initCanvas: function() {
var $canvas = $('.schematic-editor-canvas'); var $canvas = $('.schematic-editor-canvas');
if (!$canvas.length) return; if (!$canvas.length) {
return;
}
// Loading message
$canvas.html('<div style="padding:20px;color:#888;">Initialisiere Schaltplan-Editor...</div>');
// Calculate layout first // Calculate layout first
this.calculateLayout(); this.calculateLayout();
@ -5331,12 +5332,9 @@
var blockHtml = ''; var blockHtml = '';
var terminalHtml = ''; var terminalHtml = '';
console.log('SchematicEditor renderBlocks: Processing ' + this.equipment.length + ' equipment');
this.equipment.forEach(function(eq) { this.equipment.forEach(function(eq) {
var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); }); var carrier = self.carriers.find(function(c) { return String(c.id) === String(eq.carrier_id); });
if (!carrier) { if (!carrier) {
console.log(' Equipment #' + eq.id + ' (' + eq.label + '): No carrier found for carrier_id=' + eq.carrier_id);
return; return;
} }
if (typeof carrier._x === 'undefined' || carrier._x === null) { if (typeof carrier._x === 'undefined' || carrier._x === null) {
@ -5396,6 +5394,7 @@
var coveredByBusbar = self.isEquipmentCoveredByBusbar(eq); var coveredByBusbar = self.isEquipmentCoveredByBusbar(eq);
// Top terminals - im TE-Raster platziert (hide if busbar covers this equipment) // Top terminals - im TE-Raster platziert (hide if busbar covers this equipment)
var terminalColor = '#666'; // Grau wie die Hutschienen
if (!coveredByBusbar) { if (!coveredByBusbar) {
topTerminals.forEach(function(term, idx) { topTerminals.forEach(function(term, idx) {
// Berechne welches TE dieser Terminal belegt (0-basiert) // Berechne welches TE dieser Terminal belegt (0-basiert)
@ -5403,13 +5402,12 @@
// Terminal ist in der Mitte des jeweiligen TE // Terminal ist in der Mitte des jeweiligen TE
var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2);
var ty = y - 7; var ty = y - 7;
var termColor = self.PHASE_COLORS[term.label] || '#3498db';
terminalHtml += '<g class="schematic-terminal" '; terminalHtml += '<g class="schematic-terminal" ';
terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" '; terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" ';
terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">'; terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">';
terminalHtml += '<circle r="' + self.TERMINAL_RADIUS + '" fill="' + termColor + '" stroke="#fff" stroke-width="2" class="schematic-terminal-circle"/>'; terminalHtml += '<circle r="' + self.TERMINAL_RADIUS + '" fill="' + terminalColor + '" stroke="#fff" stroke-width="2" class="schematic-terminal-circle"/>';
terminalHtml += '<text y="-14" text-anchor="middle" fill="#aaa" font-size="11">' + self.escapeHtml(term.label) + '</text>'; terminalHtml += '<text y="-14" text-anchor="middle" fill="#aaa" font-size="11">' + self.escapeHtml(term.label) + '</text>';
terminalHtml += '</g>'; terminalHtml += '</g>';
}); });
@ -5422,13 +5420,12 @@
// Terminal ist in der Mitte des jeweiligen TE // Terminal ist in der Mitte des jeweiligen TE
var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2);
var ty = y + blockHeight + 7; var ty = y + blockHeight + 7;
var termColor = self.PHASE_COLORS[term.label] || '#27ae60';
terminalHtml += '<g class="schematic-terminal" '; terminalHtml += '<g class="schematic-terminal" ';
terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" '; terminalHtml += 'data-equipment-id="' + eq.id + '" data-terminal-id="' + term.id + '" ';
terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">'; terminalHtml += 'transform="translate(' + tx + ',' + ty + ')">';
terminalHtml += '<circle r="' + self.TERMINAL_RADIUS + '" fill="' + termColor + '" stroke="#fff" stroke-width="2" class="schematic-terminal-circle"/>'; terminalHtml += '<circle r="' + self.TERMINAL_RADIUS + '" fill="' + terminalColor + '" stroke="#fff" stroke-width="2" class="schematic-terminal-circle"/>';
terminalHtml += '<text y="21" text-anchor="middle" fill="#aaa" font-size="11">' + self.escapeHtml(term.label) + '</text>'; terminalHtml += '<text y="21" text-anchor="middle" fill="#aaa" font-size="11">' + self.escapeHtml(term.label) + '</text>';
terminalHtml += '</g>'; terminalHtml += '</g>';
}); });
@ -5447,11 +5444,6 @@
var html = ''; var html = '';
var renderedCount = 0; var renderedCount = 0;
// Debug: list all connections with is_rail
var busbars = this.connections.filter(function(c) { return parseInt(c.is_rail) === 1; });
console.log('SchematicEditor renderBusbars: Found ' + busbars.length + ' busbars in connections:', busbars);
console.log('SchematicEditor renderBusbars: Carriers available:', this.carriers.map(function(c) { return {id: c.id, _x: c._x, _y: c._y}; }));
// First, group busbars by carrier and position for stacking // First, group busbars by carrier and position for stacking
var busbarsByCarrierAndPos = {}; var busbarsByCarrierAndPos = {};
this.connections.forEach(function(conn) { this.connections.forEach(function(conn) {
@ -5612,8 +5604,6 @@
', x=' + startX + ', y=' + busbarY + ', width=' + width + ', color=' + color); ', x=' + startX + ', y=' + busbarY + ', width=' + width + ', color=' + color);
}); });
console.log('SchematicEditor renderBusbars: Rendered ' + renderedCount + ' busbars');
$layer.html(html); $layer.html(html);
}, },
@ -5625,8 +5615,6 @@
var html = ''; var html = '';
var renderedCount = 0; var renderedCount = 0;
console.log('SchematicEditor renderConnections: Processing ' + this.connections.length + ' connections');
this.connections.forEach(function(conn, connIndex) { this.connections.forEach(function(conn, connIndex) {
// Check is_rail as integer (PHP may return string "1" or "0") // Check is_rail as integer (PHP may return string "1" or "0")
if (parseInt(conn.is_rail) === 1) { if (parseInt(conn.is_rail) === 1) {
@ -5844,7 +5832,6 @@
renderedCount++; renderedCount++;
}); });
console.log('SchematicEditor renderConnections: Rendered ' + renderedCount + ' connections');
$layer.html(html); $layer.html(html);
// Bind click events to SVG connection elements (must be done after rendering) // Bind click events to SVG connection elements (must be done after rendering)
@ -8205,6 +8192,213 @@
$('.schematic-equipment-popup').remove(); $('.schematic-equipment-popup').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;
}
// Create popup
var popupHtml = '<div class="schematic-carrier-popup" data-carrier-id="' + carrierId + '" style="' +
'position:fixed;left:' + x + 'px;top:' + y + 'px;' +
'background:#2d2d44;border:2px solid #f39c12;border-radius:6px;padding:10px;' +
'box-shadow:0 4px 20px rgba(0,0,0,0.6);z-index:2147483647;display:flex;flex-direction:column;gap:8px;min-width:180px;">';
// Carrier info
popupHtml += '<div style="color:#fff;font-size:12px;border-bottom:1px solid #555;padding-bottom:6px;margin-bottom:2px;">';
popupHtml += '<strong>' + self.escapeHtml(carrier.label || 'Hutschiene') + '</strong>';
popupHtml += '<br><span style="color:#aaa;">' + (carrier.total_te || 12) + ' TE</span>';
popupHtml += '</div>';
// Buttons row
popupHtml += '<div style="display:flex;gap:8px;">';
// Edit button
popupHtml += '<button type="button" class="schematic-carrier-popup-edit" style="' +
'flex:1;background:#f39c12;color:#fff;border:none;border-radius:4px;padding:8px 12px;' +
'cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;gap:5px;">' +
'<i class="fas fa-edit"></i> Bearbeiten</button>';
// Delete button
popupHtml += '<button type="button" class="schematic-carrier-popup-delete" style="' +
'flex:1;background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:8px 12px;' +
'cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;gap:5px;">' +
'<i class="fas fa-trash"></i> Löschen</button>';
popupHtml += '</div></div>';
$('body').append(popupHtml);
// Bind button events
$('.schematic-carrier-popup-edit').on('click', function(e) {
e.stopPropagation();
self.editCarrier(carrierId);
});
$('.schematic-carrier-popup-delete').on('click', function(e) {
e.stopPropagation();
self.hideCarrierPopup();
self.deleteCarrier(carrierId);
});
// Adjust position if popup goes off screen
var $popup = $('.schematic-carrier-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');
}
},
hideCarrierPopup: function() {
$('.schematic-carrier-popup').remove();
},
editCarrier: function(carrierId) {
var self = this;
// Hide popup first
this.hideCarrierPopup();
// Find carrier data
var carrier = this.carriers.find(function(c) { return parseInt(c.id) === parseInt(carrierId); });
if (!carrier) return;
// Load panels for dropdown
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php',
data: { action: 'list', anlage_id: self.anlageId },
dataType: 'json',
success: function(response) {
var panels = response.success ? response.panels : [];
self.showEditCarrierDialog(carrier, panels);
}
});
},
showEditCarrierDialog: function(carrier, panels) {
var self = this;
// Remove existing dialog
$('.schematic-edit-overlay').remove();
var html = '<div class="schematic-edit-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:2147483647;display:flex;align-items:center;justify-content:center;">';
html += '<div class="schematic-edit-dialog" style="background:#2d2d44;border:2px solid #f39c12;border-radius:8px;padding:20px;min-width:350px;max-width:450px;">';
html += '<h3 style="color:#fff;margin:0 0 15px 0;padding-bottom:10px;border-bottom:1px solid #555;">Hutschiene bearbeiten</h3>';
html += '<table style="width:100%;border-collapse:collapse;">';
// Label
html += '<tr><td style="color:#aaa;padding:8px 0;">Bezeichnung</td>';
html += '<td><input type="text" class="carrier-edit-label" value="' + self.escapeHtml(carrier.label || '') + '" style="width:100%;padding:8px;background:#1a1a2e;border:1px solid #555;border-radius:4px;color:#fff;"></td></tr>';
// Total TE
html += '<tr><td style="color:#aaa;padding:8px 0;">Kapazität (TE)</td>';
html += '<td><input type="number" class="carrier-edit-total-te" value="' + (carrier.total_te || 12) + '" min="1" max="72" style="width:100%;padding:8px;background:#1a1a2e;border:1px solid #555;border-radius:4px;color:#fff;"></td></tr>';
// Panel dropdown
if (panels && panels.length > 0) {
html += '<tr><td style="color:#aaa;padding:8px 0;">Feld</td>';
html += '<td><select class="carrier-edit-panel" style="width:100%;padding:8px;background:#1a1a2e;border:1px solid #555;border-radius:4px;color:#fff;">';
html += '<option value="0">-- Kein Feld --</option>';
panels.forEach(function(p) {
var selected = (carrier.panel_id == p.id) ? ' selected' : '';
html += '<option value="' + p.id + '"' + selected + '>' + self.escapeHtml(p.label) + '</option>';
});
html += '</select></td></tr>';
}
html += '</table>';
// Buttons
html += '<div style="margin-top:20px;display:flex;gap:10px;justify-content:flex-end;">';
html += '<button type="button" class="edit-dialog-save" style="background:#f39c12;color:#fff;border:none;border-radius:4px;padding:10px 20px;cursor:pointer;font-size:14px;"><i class="fas fa-save"></i> Speichern</button>';
html += '<button type="button" class="edit-dialog-cancel" style="background:#555;color:#fff;border:none;border-radius:4px;padding:10px 20px;cursor:pointer;font-size:14px;">Abbrechen</button>';
html += '</div>';
html += '</div></div>';
$('body').append(html);
// Save handler
$('.edit-dialog-save').on('click', function() {
var label = $('.carrier-edit-label').val();
var totalTe = parseInt($('.carrier-edit-total-te').val()) || 12;
var panelId = parseInt($('.carrier-edit-panel').val()) || 0;
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php',
method: 'POST',
data: {
action: 'update',
carrier_id: carrier.id,
label: label,
total_te: totalTe,
panel_id: panelId
},
dataType: 'json',
success: function(response) {
if (response.success) {
$('.schematic-edit-overlay').remove();
self.loadData(); // Reload all data
} else {
alert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
}
}
});
});
// Cancel handler
$('.edit-dialog-cancel, .schematic-edit-overlay').on('click', function(e) {
if (e.target === this) {
$('.schematic-edit-overlay').remove();
}
});
},
deleteCarrier: function(carrierId) {
var self = this;
// Find carrier data for confirmation
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() {
$.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php',
method: 'POST',
data: {
action: 'delete',
carrier_id: carrierId
},
dataType: 'json',
success: function(response) {
if (response.success) {
self.loadData(); // Reload all data
} else {
alert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
}
}
});
});
},
editEquipment: function(equipmentId) { editEquipment: function(equipmentId) {
var self = this; var self = this;