fix(schematic): Terminal-Farbpropagierung, Auto-Naming, PWA-Abgänge

- buildTerminalPhaseMap: Schritt 1b - Leitungen mit expliziter Farbe als
  Startpunkte (nur Gerät→Gerät, keine Abgänge)
- buildTerminalPhaseMap: Block-Durchreichung (Top↔Bottom) entfernt
- buildTerminalPhaseMap: Junction-Verbindungen (Terminal→Leitung)
  bidirektional verarbeitet via _connectionById Index
- PWA: Abgangs-Rendering mit Index-Fallback wenn source_terminal_id fehlt
- PWA: Abgangs-Labels max-height 130px, min-height 30px
- Auto-Naming: EquipmentCarrier create/update → 'R' + count
- Auto-Naming: EquipmentPanel update → 'Feld ' + count
- pwa_api.php: Hardcoded Fallbacks 'Feld'/'Hutschiene' entfernt
- pwa.js: Hutschiene Auto-Naming dynamisch aus Panel-Carrier-Anzahl
- kundenkarte.js: Carrier-Dialog Placeholder 'z.B. R1 (automatisch)'
- SW Cache auf v12.5 hochgezählt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-17 09:57:58 +01:00
parent 16e51a799a
commit 71272fa425
134 changed files with 106 additions and 35 deletions

0
COPYING Normal file → Executable file
View file

0
README.md Normal file → Executable file
View file

0
admin/about.php Normal file → Executable file
View file

0
admin/anlage_systems.php Normal file → Executable file
View file

0
admin/backup.php Normal file → Executable file
View file

0
admin/busbar_types.php Normal file → Executable file
View file

0
admin/medium_types.php Normal file → Executable file
View file

0
ajax/anlage.php Normal file → Executable file
View file

0
ajax/anlage_connection.php Normal file → Executable file
View file

0
ajax/anlage_docs.php Normal file → Executable file
View file

0
ajax/anlage_images.php Normal file → Executable file
View file

0
ajax/anlage_tooltip.php Normal file → Executable file
View file

0
ajax/audit_log.php Normal file → Executable file
View file

0
ajax/bom_generator.php Normal file → Executable file
View file

0
ajax/building_types.php Normal file → Executable file
View file

0
ajax/busbar_types.php Normal file → Executable file
View file

0
ajax/equipment.php Normal file → Executable file
View file

0
ajax/equipment_carrier.php Normal file → Executable file
View file

0
ajax/equipment_connection.php Normal file → Executable file
View file

0
ajax/equipment_panel.php Normal file → Executable file
View file

0
ajax/equipment_type_block_image.php Normal file → Executable file
View file

0
ajax/equipment_type_fields.php Normal file → Executable file
View file

0
ajax/equipment_type_icon.php Normal file → Executable file
View file

0
ajax/export_schematic_pdf.php Normal file → Executable file
View file

0
ajax/export_tree_pdf.php Normal file → Executable file
View file

0
ajax/favorite_update.php Normal file → Executable file
View file

0
ajax/field_autocomplete.php Normal file → Executable file
View file

0
ajax/file_preview.php Normal file → Executable file
View file

0
ajax/graph_data.php Normal file → Executable file
View file

0
ajax/graph_save_positions.php Normal file → Executable file
View file

0
ajax/icon_upload.php Normal file → Executable file
View file

0
ajax/medium_types.php Normal file → Executable file
View file

View file

@ -517,7 +517,7 @@ switch ($action) {
$panel = new EquipmentPanel($db);
$panel->fk_anlage = $anlageId;
$panel->label = $label ?: 'Feld';
$panel->label = $label; // PHP-Klasse vergibt Auto-Name wenn leer
$result = $panel->create($user);
if ($result > 0) {
@ -556,7 +556,7 @@ switch ($action) {
$carrier = new EquipmentCarrier($db);
$carrier->fk_anlage = $panelObj->fk_anlage;
$carrier->fk_panel = $panelId;
$carrier->label = $label ?: 'Hutschiene';
$carrier->label = $label; // PHP-Klasse vergibt Auto-Name wenn leer
$carrier->total_te = $totalTe;
$result = $carrier->create($user);

0
ajax/tree_config.php Normal file → Executable file
View file

0
ajax/type_fields.php Normal file → Executable file
View file

0
anlage_connection.php Normal file → Executable file
View file

0
build/buildzip.php Normal file → Executable file
View file

0
build/makepack-kundenkarte.conf Normal file → Executable file
View file

0
class/anlage.class.php Normal file → Executable file
View file

0
class/anlagebackup.class.php Normal file → Executable file
View file

0
class/anlageconnection.class.php Normal file → Executable file
View file

0
class/anlagefile.class.php Normal file → Executable file
View file

0
class/auditlog.class.php Normal file → Executable file
View file

0
class/busbartype.class.php Normal file → Executable file
View file

0
class/equipment.class.php Normal file → Executable file
View file

20
class/equipmentcarrier.class.php Normal file → Executable file
View file

@ -56,7 +56,7 @@ class EquipmentCarrier extends CommonObject
$error = 0;
$now = dol_now();
if (empty($this->fk_anlage) || empty($this->label)) {
if (empty($this->fk_anlage)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
@ -72,6 +72,15 @@ class EquipmentCarrier extends CommonObject
}
}
// Auto-naming wenn kein Label angegeben
if (empty($this->label)) {
$sqlCnt = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sqlCnt .= " WHERE fk_panel = ".((int) $this->fk_panel);
$resCnt = $this->db->query($sqlCnt);
$cnt = ($resCnt && ($objCnt = $this->db->fetch_object($resCnt))) ? (int) $objCnt->cnt : 0;
$this->label = 'R'.($cnt + 1);
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
@ -166,6 +175,15 @@ class EquipmentCarrier extends CommonObject
{
$error = 0;
// Auto-naming wenn kein Label angegeben
if (empty($this->label)) {
$sqlCnt = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sqlCnt .= " WHERE fk_panel = ".((int) $this->fk_panel);
$resCnt = $this->db->query($sqlCnt);
$cnt = ($resCnt && ($objCnt = $this->db->fetch_object($resCnt))) ? (int) $objCnt->cnt : 1;
$this->label = 'R'.$cnt;
}
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";

0
class/equipmentconnection.class.php Normal file → Executable file
View file

9
class/equipmentpanel.class.php Normal file → Executable file
View file

@ -163,6 +163,15 @@ class EquipmentPanel extends CommonObject
{
$error = 0;
// Auto-naming wenn kein Label angegeben
if (empty($this->label)) {
$sqlCnt = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sqlCnt .= " WHERE fk_anlage = ".((int) $this->fk_anlage);
$resCnt = $this->db->query($sqlCnt);
$cnt = ($resCnt && ($objCnt = $this->db->fetch_object($resCnt))) ? (int) $objCnt->cnt : 1;
$this->label = 'Feld '.$cnt;
}
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";

0
class/equipmenttype.class.php Normal file → Executable file
View file

0
class/favoriteproduct.class.php Normal file → Executable file
View file

0
class/mediumtype.class.php Normal file → Executable file
View file

0
class/terminalbridge.class.php Normal file → Executable file
View file

2
core/modules/modKundenKarte.class.php Normal file → Executable file
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'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '11.0.8';
$this->version = '11.1.2';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

0
css/kundenkarte.css Normal file → Executable file
View file

0
css/kundenkarte_cytoscape.css Normal file → Executable file
View file

View file

@ -981,7 +981,7 @@ body {
.terminal-label-cell {
display: flex;
justify-content: center;
min-height: 20px;
min-height: 30px;
}
/* Obere Labels (Zeile 1): am unteren Rand ausrichten (zum Terminal hin) */
@ -1006,7 +1006,7 @@ body {
font-weight: 600;
color: #fff;
line-height: 1.1;
max-height: 80px;
max-height: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

0
img/README.md Normal file → Executable file
View file

0
js/cose-base.js Normal file → Executable file
View file

0
js/cytoscape-cose-bilkent.js Normal file → Executable file
View file

0
js/cytoscape-dagre.js Normal file → Executable file
View file

0
js/cytoscape.min.js vendored Normal file → Executable file
View file

0
js/dagre.min.js vendored Normal file → Executable file
View file

82
js/kundenkarte.js Normal file → Executable file
View file

@ -5548,6 +5548,24 @@
setPhase(conn.fk_target, conn.target_terminal_id, phase, inputColor);
});
// --- Schritt 1b: Leitungen mit expliziter Farbe als Startpunkte ---
// Nur echte Verbindungen zwischen zwei Geräten (fk_source + fk_target),
// KEINE Abgänge (fk_target=NULL) — Abgang-Terminals bekommen keine Farbe
this.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) === 1) return;
if (!conn.color) return;
if (!conn.fk_source || !conn.fk_target) return; // Nur Gerät→Gerät, keine Abgänge
var phase = (conn.connection_type || '').toUpperCase();
if (!self.PHASE_COLORS[phase]) return;
var connColor = conn.color;
if (conn.source_terminal_id) {
setPhase(conn.fk_source, conn.source_terminal_id, phase, connColor);
}
if (conn.target_terminal_id) {
setPhase(conn.fk_target, conn.target_terminal_id, phase, connColor);
}
});
// --- Iterativ propagieren bis keine Änderungen mehr ---
var changed = true;
var iterations = 0;
@ -5556,28 +5574,8 @@
while (changed && iterations++ < maxIterations) {
changed = false;
// Block-Durchreichung (top ↔ bottom) - Farbe wird mitpropagiert
self.equipment.forEach(function(eq) {
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 pairCount = Math.min(topTerminals.length, bottomTerminals.length);
for (var i = 0; i < pairCount; i++) {
var topPhase = (self._terminalPhaseMap[eq.id] || {})[topTerminals[i].id];
var botPhase = (self._terminalPhaseMap[eq.id] || {})[bottomTerminals[i].id];
if (topPhase && !botPhase) {
var topColor = getColor(eq.id, topTerminals[i].id);
if (setPhase(eq.id, bottomTerminals[i].id, topPhase, topColor)) changed = true;
} else if (botPhase && !topPhase) {
var botColor = getColor(eq.id, bottomTerminals[i].id);
if (setPhase(eq.id, topTerminals[i].id, botPhase, botColor)) changed = true;
}
}
});
// Leitungen propagieren (Phase + Farbe von einem Ende zum anderen)
// Keine Block-Durchreichung (top↔bottom): Farben nur durch explizite Verbindungen
self.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) === 1) return;
if (!conn.fk_source || !conn.fk_target) return;
@ -5595,6 +5593,46 @@
}
});
// Junction-Verbindungen (Terminal→Leitung): Farbe bidirektional übertragen
self.connections.forEach(function(conn) {
if (parseInt(conn.is_rail) === 1) return;
if (!conn.junction_connection_id) return;
if (!conn.fk_source || !conn.source_terminal_id) return;
var jConn = self._connectionById[String(conn.junction_connection_id)];
if (!jConn) return;
var srcPhase = (self._terminalPhaseMap[conn.fk_source] || {})[conn.source_terminal_id];
// Farbe der Ziel-Leitung ermitteln (aus einem ihrer Endpoints)
var jPhase = null, jColor = null;
if (jConn.fk_source && jConn.source_terminal_id) {
jPhase = (self._terminalPhaseMap[jConn.fk_source] || {})[jConn.source_terminal_id];
jColor = getColor(jConn.fk_source, jConn.source_terminal_id);
}
if (!jPhase && jConn.fk_target && jConn.target_terminal_id) {
jPhase = (self._terminalPhaseMap[jConn.fk_target] || {})[jConn.target_terminal_id];
jColor = getColor(jConn.fk_target, jConn.target_terminal_id);
}
// Ziel-Leitung hat Phase → Source-Terminal übernimmt sie
if (jPhase && !srcPhase) {
if (setPhase(conn.fk_source, conn.source_terminal_id, jPhase, jColor)) changed = true;
}
// Source-Terminal hat Phase → auf Endpoints der Ziel-Leitung übertragen
if (srcPhase && !jPhase) {
var srcColor = getColor(conn.fk_source, conn.source_terminal_id);
if (jConn.fk_source && jConn.source_terminal_id &&
!(self._terminalPhaseMap[jConn.fk_source] || {})[jConn.source_terminal_id]) {
if (setPhase(jConn.fk_source, jConn.source_terminal_id, srcPhase, srcColor)) changed = true;
}
if (jConn.fk_target && jConn.target_terminal_id &&
!(self._terminalPhaseMap[jConn.fk_target] || {})[jConn.target_terminal_id]) {
if (setPhase(jConn.fk_target, jConn.target_terminal_id, srcPhase, srcColor)) changed = true;
}
}
});
// Busbar-Verteilung: Nur Phasen verteilen die tatsächlich eingespeist sind
self.connections.forEach(function(busbar) {
if (parseInt(busbar.is_rail) !== 1) return;
@ -9076,7 +9114,7 @@
html += '<div class="schematic-dialog" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;z-index:100001;min-width:300px;">';
html += '<h3 style="margin:0 0 15px 0;color:#fff;">Neue Hutschiene hinzufügen</h3>';
html += '<div style="margin-bottom:12px;"><label style="display:block;color:#aaa;margin-bottom:4px;">Bezeichnung:</label>';
html += '<input type="text" class="dialog-carrier-label" value="" placeholder="z.B. H1" style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;"/></div>';
html += '<input type="text" class="dialog-carrier-label" value="" placeholder="z.B. R1 (automatisch)" style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;"/></div>';
html += '<div style="margin-bottom:12px;"><label style="display:block;color:#aaa;margin-bottom:4px;">Teilungseinheiten (TE):</label>';
html += '<input type="number" class="dialog-carrier-te" value="12" min="1" max="48" style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;"/></div>';
html += '<div style="display:flex;gap:10px;justify-content:flex-end;">';

0
js/kundenkarte_cytoscape.js Normal file → Executable file
View file

0
js/layout-base.js Normal file → Executable file
View file

0
js/pathfinding.min.js vendored Normal file → Executable file
View file

View file

@ -902,7 +902,7 @@
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:1;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const termId = topTerms[t] ? topTerms[t].id : ('t' + (t + 1));
const topOut = eqTopOutputs.find(o => o.source_terminal_id === termId) || null;
const topOut = eqTopOutputs.find(o => o.source_terminal_id === termId) || eqTopOutputs[t] || null;
if (topOut && topOut.output_label && (!topOut.bundled_terminals || widthTe <= 1)) {
const cableInfo = buildCableInfo(topOut);
@ -951,7 +951,7 @@
// Input/Output per Terminal-ID finden
const inp = eqTopInputs.find(i => i.target_terminal_id === termId) || null;
const topOut = bundledTop || eqTopOutputs.find(o => o.source_terminal_id === termId) || null;
const topOut = bundledTop || eqTopOutputs.find(o => o.source_terminal_id === termId) || eqTopOutputs[t] || null;
if (bundledTop && widthTe > 1) {
if (t === 0) {
@ -1083,7 +1083,7 @@
const termId = botTerms[t] ? botTerms[t].id : ('t' + (widthTe + t + 1));
// Input/Output per Terminal-ID finden
const out = bundledBottom || eqBottomOutputs.find(o => o.source_terminal_id === termId) || null;
const out = bundledBottom || eqBottomOutputs.find(o => o.source_terminal_id === termId) || eqBottomOutputs[t] || null;
const inp = eqBottomInputs.find(i => i.target_terminal_id === termId) || null;
if (bundledBottom && widthTe > 1) {
@ -1169,7 +1169,7 @@
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:5;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const termId = botTerms[t] ? botTerms[t].id : ('t' + (widthTe + t + 1));
const out = eqBottomOutputs.find(o => o.source_terminal_id === termId) || null;
const out = eqBottomOutputs.find(o => o.source_terminal_id === termId) || eqBottomOutputs[t] || null;
if (out && out.output_label && (!out.bundled_terminals || widthTe <= 1)) {
const cableInfo = buildCableInfo(out);
@ -1498,7 +1498,13 @@
}
const totalTe = parseInt(teBtn.data('te'));
const label = $('#carrier-label').val().trim() || 'Hutschiene';
const panelId = App.editCarrierId
? (App.carriers.find(c => c.id == App.editCarrierId)?.fk_panel || App.currentPanelId)
: App.currentPanelId;
const panelCarrierCount = App.carriers.filter(c => c.fk_panel == panelId).length;
const inputLabel = $('#carrier-label').val().trim();
// CREATE: +1 weil neuer Carrier noch nicht in der Liste; UPDATE: inkl. sich selbst gezählt
const label = inputLabel || (App.editCarrierId ? 'R' + panelCarrierCount : 'R' + (panelCarrierCount + 1));
closeModal('add-carrier');

0
kundenkarteindex.php Normal file → Executable file
View file

0
langs/de_DE/kundenkarte.lang Normal file → Executable file
View file

0
langs/en_US/kundenkarte.lang Normal file → Executable file
View file

0
lib/graph_view.lib.php Normal file → Executable file
View file

0
lib/kundenkarte.lib.php Normal file → Executable file
View file

0
modulebuilder.txt Normal file → Executable file
View file

View file

@ -410,6 +410,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
</script>
<script src="js/pwa.js?v=5.8"></script>
<script src="js/pwa.js?v=5.9"></script>
</body>
</html>

0
sql/data.sql Normal file → Executable file
View file

0
sql/data_building_types.sql Normal file → Executable file
View file

0
sql/data_busbar_types.sql Normal file → Executable file
View file

0
sql/data_medium_types.sql Normal file → Executable file
View file

0
sql/data_terminal_types.sql Normal file → Executable file
View file

0
sql/dolibarr_allversions.sql Normal file → Executable file
View file

0
sql/llx_c_kundenkarte_anlage_system.key.sql Normal file → Executable file
View file

0
sql/llx_c_kundenkarte_anlage_system.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_accessory.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_accessory.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_connection.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_connection.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_contact.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_files.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_files.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_type.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_type.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_type_field.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_anlage_type_field.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_audit_log.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_audit_log.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_building_type.key.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_building_type.sql Normal file → Executable file
View file

0
sql/llx_kundenkarte_busbar_type.key.sql Normal file → Executable file
View file

Some files were not shown because too many files have changed in this diff Show more