diff --git a/CLAUDE.md b/CLAUDE.md index 4b222df..065505d 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,7 +132,7 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte. ### Libraries (lib/) - `kundenkarte.lib.php` - Allgemeine Hilfs-Funktionen - `graph_view.lib.php` - Shared Graph-Funktionen (Toolbar, Container, Legende) -- `wiring_diagram.lib.php` - Leitungslaufplan (WiringDiagramAnalyzer + WiringDiagramRenderer) +- `wiring_diagram.lib.php` - Leitungslaufplan + Verteilungs-Tabellen (~2.130 Zeilen) ### AJAX-Endpunkte (ajax/) — 30+ Dateien - `anlage.php` - Anlagen CRUD @@ -150,9 +150,9 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte. - `pwa_api.php` - PWA-Endpoints ### Frontend -- `js/kundenkarte.js` - Haupt-JS (~11.000 Zeilen) +- `js/kundenkarte.js` - Haupt-JS (~15.600 Zeilen) - `js/kundenkarte_cytoscape.js` - Graph-JS (~900 Zeilen) -- `js/pwa.js` - PWA-JS (~1.950 Zeilen) +- `js/pwa.js` - PWA-JS (~3.400 Zeilen) - `css/kundenkarte.css` - Alle Styles (Dark Mode Theme) - `css/pwa.css` - PWA-Styles @@ -183,7 +183,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat - `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA - `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter) - `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen -- `sw.js` - Service Worker für Offline-Cache (v6.1) +- `sw.js` - Service Worker für Offline-Cache (v12.4) - `manifest.json` - Web App Manifest für Installation ### Workflow @@ -260,7 +260,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat - `bundled_terminals = 'all'` in Connection bedeutet: Alle Terminals belegt - Im Editor: Ein Pfeil spannt über alle Terminals des Equipment - Label wird zentriert über alle Terminals angezeigt -- Checkbox "Alle bündeln" nur bei Equipment mit >1 Terminal sichtbar +- Checkbox "Alle Terminals bündeln" im Abgang-Dialog (Website + PWA), nur bei Equipment mit >1 Terminal ### Terminal-Konfiguration (v7.5) - `terminals_config` JSON im Equipment-Typ definiert Terminal-Positionen @@ -348,16 +348,18 @@ Normgerechter Stromlaufplan in aufgelöster Darstellung (DIN EN 61082) als PDF-E **Komplett separates Feature** — kann durch Löschen von 2 Dateien + 8 Zeilen rückstandsfrei entfernt werden. ### Dateien -- `lib/wiring_diagram.lib.php` — Kernlogik (~1240 Zeilen) +- `lib/wiring_diagram.lib.php` — Kernlogik (~2.130 Zeilen) - `WiringDiagramAnalyzer` — Lädt Daten, baut Phase-Map (PHP-Port), tracet Strompfade - `WiringDiagramRenderer` — Zeichnet PDF mit TCPDF - `ajax/export_wiring_diagram_pdf.php` — Endpoint - Buttons in `tabs/anlagen.php` + `tabs/contact_anlagen.php` (je 4 Zeilen) -### PDF-Inhalt (3 Teile) -1. **Leitungslaufplan** — L1/L2/L3 horizontal oben, vertikale Strompfade pro Abgang, FI/RCD + LS-Symbole, Abgang-Pfeile, N/PE unten -2. **Abgangsverzeichnis** — Tabelle pro Hutschiene mit: Abg.Nr, Bezeichnung, Phase, Absicherung, Kabel, Schutzgerät -3. **Legende** — Phasenfarben DIN VDE, VDE-Symbole, Norm-Referenzen +### PDF-Inhalt (5 Teile) +1. **Leitungslaufplan** (A3 quer) — L1/L2/L3 horizontal oben, vertikale Strompfade pro Abgang, FI/RCD + LS-Symbole, Abgang-Pfeile, N/PE unten +2. **Abgangsverzeichnis** (A3 quer) — Tabelle pro Hutschiene mit: Abg.Nr, Bezeichnung, Phase, Absicherung, Kabel, Schutzgerät +3. **Kundenansicht** (A4 hoch) — `renderKundenansicht()` — Einfache Tabelle: Nr | Verbraucher | Räumlichkeit, gruppiert nach Feld/Reihe +4. **Technikeransicht** (A4 hoch) — `renderTechnikeransicht()` — Erweiterte Tabelle: R.Klem | FI | Nr | Verbraucher | Räumlichkeit | Typ +5. **Mini-Legende** — Phasenfarben DIN VDE auf Seite 1 unten links ### Abgangsnummer-Format `R{Reihe}.{Position}` z.B. `R1.3` = Carrier-Position 1, Equipment-TE-Position 3 @@ -380,6 +382,28 @@ Pro Abgang (Connection mit `fk_target = NULL`): - FI/RCD: Rechteck mit Kreis + Vertikallinie (Differenzstrom-Symbol) - Gezeichnet mit TCPDF-Primitiven (Line, Rect, Circle, Polygon) +## Räumlichkeit / output_location (v8.6) + +### Übersicht +Zusätzliches Textfeld am Abgang (Output-Connection) für den Raum/Ort des Verbrauchers (z.B. "Küche", "Bad OG"). + +### Datenbank +- Spalte `output_location` (varchar 255) in `llx_kundenkarte_equipment_connection` +- Migration: `migrate_v1110_output_location()` in `modKundenKarte.class.php` + +### Backend +- `EquipmentConnection::$output_location` — Property, in create/update/fetch +- `ajax/equipment_connection.php` — create_output + update + list_all +- `ajax/pwa_api.php` — get_carrier_equipment + create_connection + update_connection + +### Frontend +- **Website**: Eingabefeld im `renderAbgangDialog()` (kundenkarte.js) +- **PWA**: Eingabefeld `#conn-location` im Connection-Modal (pwa.php + pwa.js) +- **Anzeige im Schaltplan**: + - Website SVG: `` kursiv nach Label mit ` · ` Trennzeichen + - PWA Grid: `` kursiv unter dem Label-Text +- **PDF**: In Kundenansicht + Technikeransicht als eigene Tabellenspalte + ## Select2 mit Kategorie-Filter ### Problem & Lösung diff --git a/ChangeLog.md b/ChangeLog.md old mode 100644 new mode 100755 diff --git a/admin/anlage_types.php b/admin/anlage_types.php old mode 100644 new mode 100755 diff --git a/admin/building_types.php b/admin/building_types.php old mode 100644 new mode 100755 diff --git a/admin/equipment_types.php b/admin/equipment_types.php old mode 100644 new mode 100755 diff --git a/admin/setup.php b/admin/setup.php old mode 100644 new mode 100755 diff --git a/ajax/anlage_accessory.php b/ajax/anlage_accessory.php old mode 100644 new mode 100755 diff --git a/ajax/equipment.php b/ajax/equipment.php index c0d0ba9..a0cef2d 100644 --- a/ajax/equipment.php +++ b/ajax/equipment.php @@ -267,6 +267,7 @@ switch ($action) { 'block_color' => $eq->getBlockColor(), 'field_values' => $eq->getFieldValues(), 'fk_product' => $eq->fk_product, + 'fk_protection' => $eq->fk_protection, 'product_ref' => $productRef, 'product_label' => $productLabel ); diff --git a/ajax/equipment_connection.php b/ajax/equipment_connection.php index 91f5ff3..fda90ef 100644 --- a/ajax/equipment_connection.php +++ b/ajax/equipment_connection.php @@ -187,6 +187,7 @@ switch ($action) { if (GETPOSTISSET('connection_type')) $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); if (GETPOSTISSET('color')) $connection->color = GETPOST('color', 'alphanohtml'); if (GETPOSTISSET('output_label')) $connection->output_label = GETPOST('output_label', 'alphanohtml'); + if (GETPOSTISSET('output_location')) $connection->output_location = GETPOST('output_location', 'alphanohtml'); if (GETPOSTISSET('medium_type')) $connection->medium_type = GETPOST('medium_type', 'alphanohtml'); if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml'); if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml'); @@ -303,6 +304,7 @@ switch ($action) { $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); $connection->color = GETPOST('color', 'alphanohtml'); $connection->output_label = GETPOST('output_label', 'alphanohtml'); + $connection->output_location = GETPOST('output_location', 'alphanohtml'); $connection->medium_type = GETPOST('medium_type', 'alphanohtml'); $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml'); $connection->medium_length = GETPOST('medium_length', 'alphanohtml'); @@ -367,6 +369,7 @@ switch ($action) { 'connection_type' => $obj->connection_type, 'color' => $obj->color ?: '#3498db', 'output_label' => $obj->output_label, + 'output_location' => isset($obj->output_location) ? $obj->output_location : null, 'medium_type' => $obj->medium_type, 'medium_spec' => $obj->medium_spec, 'medium_length' => $obj->medium_length, diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php old mode 100644 new mode 100755 index bb5d072..0215174 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -291,7 +291,7 @@ switch ($action) { $outputsData = array(); if (!empty($equipmentData)) { $equipmentIds = array_map(function($e) { return (int) $e['id']; }, $equipmentData); - $sql = "SELECT rowid, fk_source, output_label, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id, bundled_terminals"; + $sql = "SELECT rowid, fk_source, output_label, output_location, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id, bundled_terminals"; $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; $sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")"; $sql .= " AND fk_target IS NULL"; @@ -344,6 +344,7 @@ switch ($action) { 'id' => $obj->rowid, 'fk_source' => $obj->fk_source, 'output_label' => $obj->output_label, + 'output_location' => isset($obj->output_location) ? $obj->output_location : '', 'medium_type' => $obj->medium_type, 'medium_spec' => $obj->medium_spec, 'medium_length' => $obj->medium_length, @@ -360,7 +361,7 @@ switch ($action) { // Einspeisungen laden (Connections mit fk_source IS NULL = Inputs) $inputsData = array(); if (!empty($equipmentData)) { - $sql = "SELECT rowid, fk_target, output_label, connection_type, color"; + $sql = "SELECT rowid, fk_target, target_terminal_id, output_label, connection_type, color"; $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; $sql .= " WHERE fk_target IN (".implode(',', $equipmentIds).")"; $sql .= " AND fk_source IS NULL"; @@ -371,6 +372,7 @@ switch ($action) { $inputsData[] = array( 'id' => $obj->rowid, 'fk_target' => $obj->fk_target, + 'target_terminal_id' => $obj->target_terminal_id ?: '', 'output_label' => $obj->output_label, 'connection_type' => $obj->connection_type, 'color' => $obj->color @@ -379,31 +381,64 @@ switch ($action) { } } - // Verbindungen zwischen Equipment laden (mit path_data für Linien-Anzeige) + // Verbindungen zwischen Equipment laden (alle, für Phasen-Propagierung + Linien-Anzeige) $connectionsData = array(); if (!empty($equipmentData)) { - $sql = "SELECT rowid, fk_source, fk_target, source_terminal_id, target_terminal_id, connection_type, color, path_data"; + $sql = "SELECT rowid, fk_source, fk_target, source_terminal_id, target_terminal_id, connection_type, color, path_data, is_rail"; $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; $sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")"; $sql .= " AND fk_target IS NOT NULL"; $sql .= " AND fk_target IN (".implode(',', $equipmentIds).")"; + $sql .= " AND is_rail = 0"; $sql .= " AND status = 1"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { - // Nur Verbindungen mit gezeichnetem Pfad laden - if (!empty($obj->path_data)) { - $connectionsData[] = array( - 'id' => $obj->rowid, - 'fk_source' => $obj->fk_source, - 'fk_target' => $obj->fk_target, - 'source_terminal_id' => $obj->source_terminal_id, - 'target_terminal_id' => $obj->target_terminal_id, - 'connection_type' => $obj->connection_type, - 'color' => $obj->color, - 'path_data' => $obj->path_data - ); + $connectionsData[] = array( + 'id' => $obj->rowid, + 'fk_source' => $obj->fk_source, + 'fk_target' => $obj->fk_target, + 'source_terminal_id' => $obj->source_terminal_id, + 'target_terminal_id' => $obj->target_terminal_id, + 'connection_type' => $obj->connection_type, + 'color' => $obj->color, + 'path_data' => $obj->path_data ?: null + ); + } + } + } + + // Busbars (Phasenschienen) laden für Farbpropagierung + $busbarsData = array(); + if (!empty($carriersData)) { + $carrierIds = array_map(function($c) { return (int) $c['id']; }, $carriersData); + $sql = "SELECT c.rowid, c.fk_carrier, c.rail_start_te, c.rail_end_te, c.rail_phases,"; + $sql .= " c.excluded_te, c.position_y, c.connection_type,"; + $sql .= " bt.phases_config as busbar_phases_config"; + $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection as c"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type as bt ON c.fk_busbar_type = bt.rowid"; + $sql .= " WHERE c.fk_carrier IN (".implode(',', $carrierIds).")"; + $sql .= " AND c.is_rail = 1"; + $sql .= " AND c.status = 1"; + $sql .= " ORDER BY c.position_y ASC"; + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $phasesConfig = null; + if (!empty($obj->busbar_phases_config)) { + $phasesConfig = json_decode($obj->busbar_phases_config, true); } + $busbarsData[] = array( + 'id' => $obj->rowid, + 'fk_carrier' => $obj->fk_carrier, + 'rail_start_te' => (int) $obj->rail_start_te, + 'rail_end_te' => (int) $obj->rail_end_te, + 'rail_phases' => $obj->rail_phases, + 'excluded_te' => $obj->excluded_te ?: '', + 'position_y' => (int) $obj->position_y, + 'connection_type' => $obj->connection_type, + 'phases_config' => $phasesConfig + ); } } } @@ -458,6 +493,7 @@ switch ($action) { $response['outputs'] = $outputsData; $response['inputs'] = $inputsData; $response['connections'] = $connectionsData; + $response['busbars'] = $busbarsData; $response['types'] = $typesData; $response['field_meta'] = $fieldMetaData; break; @@ -837,6 +873,7 @@ switch ($action) { $conn->connection_type = $connectionType; $conn->color = GETPOST('color', 'alphanohtml'); $conn->output_label = $outputLabel; + $conn->output_location = GETPOST('output_location', 'alphanohtml'); $conn->fk_carrier = $eq->fk_carrier; if ($direction === 'input') { @@ -892,6 +929,7 @@ switch ($action) { $conn->connection_type = GETPOST('connection_type', 'alphanohtml'); $conn->color = GETPOST('color', 'alphanohtml'); $conn->output_label = GETPOST('output_label', 'alphanohtml'); + if (GETPOSTISSET('output_location')) $conn->output_location = GETPOST('output_location', 'alphanohtml'); $conn->medium_type = GETPOST('medium_type', 'alphanohtml'); $conn->medium_spec = GETPOST('medium_spec', 'alphanohtml'); $conn->medium_length = GETPOST('medium_length', 'alphanohtml'); diff --git a/class/anlageaccessory.class.php b/class/anlageaccessory.class.php old mode 100644 new mode 100755 diff --git a/class/anlagetype.class.php b/class/anlagetype.class.php old mode 100644 new mode 100755 diff --git a/class/buildingtype.class.php b/class/buildingtype.class.php old mode 100644 new mode 100755 diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php index 0326414..50c80b3 100644 --- a/class/equipmentconnection.class.php +++ b/class/equipmentconnection.class.php @@ -30,6 +30,7 @@ class EquipmentConnection extends CommonObject // Output/endpoint info public $output_label; + public $output_location; // Räumlichkeit/Örtlichkeit des Verbrauchers // Medium info (cable, wire, etc.) public $medium_type; @@ -91,7 +92,7 @@ class EquipmentConnection extends CommonObject $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; $sql .= "entity, fk_source, source_terminal, source_terminal_id, bundled_terminals, fk_target, target_terminal, target_terminal_id,"; - $sql .= " connection_type, color, output_label,"; + $sql .= " connection_type, color, output_label, output_location,"; $sql .= " medium_type, medium_spec, medium_length,"; $sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, num_lines, fk_busbar_type, fk_carrier, position_y, path_data,"; $sql .= " note_private, status, date_creation, fk_user_creat"; @@ -107,6 +108,7 @@ class EquipmentConnection extends CommonObject $sql .= ", ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL"); $sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL"); + $sql .= ", ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL"); $sql .= ", ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL"); $sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL"); $sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL"); @@ -178,6 +180,7 @@ class EquipmentConnection extends CommonObject $this->connection_type = $obj->connection_type; $this->color = $obj->color; $this->output_label = $obj->output_label; + $this->output_location = isset($obj->output_location) ? $obj->output_location : null; $this->medium_type = $obj->medium_type; $this->medium_spec = $obj->medium_spec; $this->medium_length = $obj->medium_length; @@ -234,6 +237,7 @@ class EquipmentConnection extends CommonObject $sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL"); $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL"); + $sql .= ", output_location = ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL"); $sql .= ", medium_type = ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL"); $sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL"); $sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL"); diff --git a/class/equipmenttype.class.php b/class/equipmenttype.class.php index 817ae4e..7474996 100644 --- a/class/equipmenttype.class.php +++ b/class/equipmenttype.class.php @@ -328,6 +328,7 @@ class EquipmentType extends CommonObject $type->picto = $obj->picto; $type->icon_file = $obj->icon_file; $type->block_image = $obj->block_image; + $type->terminals_config = $obj->terminals_config; $type->is_system = $obj->is_system; $type->position = $obj->position; $type->active = $obj->active; diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php index 6d527c1..cabe596 100644 --- a/core/modules/modKundenKarte.class.php +++ b/core/modules/modKundenKarte.class.php @@ -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 = '9.7'; + $this->version = '11.0.8'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -662,6 +662,9 @@ class modKundenKarte extends DolibarrModules // v11.0.0: Busbar type reference and num_lines for connections $this->migrate_v1100_busbar_fields(); + + // v11.1.0: Räumlichkeit (output_location) für Abgänge + $this->migrate_v1110_output_location(); } /** @@ -1093,6 +1096,24 @@ class modKundenKarte extends DolibarrModules } } + /** + * Migration v11.1.0: Räumlichkeit (output_location) für Abgänge + */ + private function migrate_v1110_output_location() + { + $table = MAIN_DB_PREFIX."kundenkarte_equipment_connection"; + + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + return; + } + + $resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'output_location'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + $this->db->query("ALTER TABLE ".$table." ADD COLUMN output_location varchar(255) DEFAULT NULL AFTER output_label"); + } + } + /** * Function called when module is disabled. * Remove from database constants, boxes and permissions from Dolibarr database. diff --git a/css/kundenkarte.css b/css/kundenkarte.css index f4c4fca..1d5038c 100644 --- a/css/kundenkarte.css +++ b/css/kundenkarte.css @@ -2151,10 +2151,30 @@ body.kundenkarte-drag-active * { .schematic-editor-actions { display: flex !important; flex-wrap: wrap !important; - gap: 10px !important; + gap: 5px !important; align-items: center !important; } +/* Einheitliche Größe für alle Toolbar-Buttons (button + a) */ +.schematic-editor-actions > button, +.schematic-editor-actions > a { + padding: 5px 8px !important; + background: #333 !important; + border: 1px solid #555 !important; + border-radius: 3px !important; + cursor: pointer !important; + font-size: 12px !important; + line-height: 1.4 !important; + height: 30px !important; + box-sizing: border-box !important; + display: inline-flex !important; + align-items: center !important; + gap: 4px !important; + text-decoration: none !important; + white-space: nowrap !important; + font-family: inherit !important; +} + .schematic-editor-toggle { color: #3498db !important; text-decoration: none !important; diff --git a/css/pwa.css b/css/pwa.css old mode 100644 new mode 100755 index 441e20c..4b705af --- a/css/pwa.css +++ b/css/pwa.css @@ -905,12 +905,14 @@ body { display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 2px; cursor: pointer; - padding: 3px 2px; + padding: 3px 0; border-radius: 4px; transition: background 0.15s; min-width: 0; + text-align: center; } /* Zeile 2 (obere Terminals): direkt am Equipment (margin-bottom negativ) */ @@ -950,6 +952,8 @@ body { width: 0; height: 0; flex-shrink: 0; + align-self: center; + margin: 0 auto; } .terminal-arrow-down { @@ -1029,6 +1033,15 @@ body { background: rgba(173, 140, 79, 0.4); } +.terminal-label .output-location { + display: block; + font-style: italic; + font-weight: normal; + font-size: 8px; + color: rgba(255,255,255,0.5); + margin-top: 1px; +} + .terminal-label .cable-info { font-weight: normal; font-size: 7px; @@ -2082,3 +2095,9 @@ body { border: 2px dashed rgba(255, 255, 255, 0.25); } +/* Leere TE-Positionen ohne Terminal (z.B. NEO 4TE aber nur 3 Terminals) */ +.terminal-point.no-terminal { + visibility: hidden; + pointer-events: none; +} + diff --git a/img/pwa-icon-192.png b/img/pwa-icon-192.png old mode 100644 new mode 100755 diff --git a/img/pwa-icon-512.png b/img/pwa-icon-512.png old mode 100644 new mode 100755 diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 8364756..e42a343 100644 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -1504,7 +1504,7 @@ data.fields.forEach(function(field) { if (field.type === 'header') { // Header row spans both columns with styling - html += '' + KundenKarte.DynamicFields.escapeHtml(field.label) + ''; + html += '' + KundenKarte.DynamicFields.escapeHtml(field.label) + ''; } else { html += '' + KundenKarte.DynamicFields.escapeHtml(field.label); if (field.required) html += ' *'; @@ -4284,11 +4284,17 @@ // Label if exists - positioned along the routing path if (conn.output_label) { + var fullLabel = conn.output_label; + if (conn.output_location) fullLabel += ' · ' + conn.output_location; var labelY = equipmentY + 35 + (connectionIndex * 12); var labelX = (sourceX + targetX) / 2; - html += ''; + var rectWidth = Math.max(50, fullLabel.length * 5 + 10); + html += ''; html += ''; html += this.escapeHtml(conn.output_label); + if (conn.output_location) { + html += ' · ' + this.escapeHtml(conn.output_location) + ''; + } html += ''; } @@ -7416,6 +7422,21 @@ } + // Schutzgruppen-Markierung (FI/RCD) + if (eq.fk_protection) { + // Geschütztes Equipment: farbiger Balken unten (wie PWA) + var protColor = self.getProtectionColor(eq.fk_protection); + blockHtml += ''; + } + // Schutzgerät selbst: linker Balken + var isProtectionDevice = self.equipment.some(function(e) { return e.fk_protection == eq.id; }); + if (isProtectionDevice) { + var deviceColor = self.getProtectionColor(eq.id); + blockHtml += ''; + } + blockHtml += ''; // Terminals (bidirectional) @@ -7790,7 +7811,8 @@ var $layer = $(this.svgElement).find('.schematic-connections-layer'); $layer.empty(); - var html = ''; + var htmlBack = ''; // Leitungen (unten) + var htmlFront = ''; // Abgänge + Eingänge (oben) var renderedCount = 0; // Get display settings @@ -7798,6 +7820,31 @@ var shadowWidth = wireWidth + 4; var hoverWidth = wireWidth + 1.5; + // Abgang-Bereiche vorab berechnen (für Badge-Kollisionserkennung) + this._outputAreas = []; + this.connections.forEach(function(conn) { + if (!conn.fk_target && !conn.path_data && conn.fk_source) { + var eq = self.getEquipmentById(conn.fk_source); + if (!eq) return; + var terms = self.getTerminals(eq); + var termId = conn.source_terminal_id || 't2'; + var pos = self.getTerminalPosition(eq, termId, terms); + if (!pos) return; + var labelText = conn.output_label || ''; + if (conn.output_location) labelText += ' · ' + conn.output_location; + var cableText = ((conn.medium_type || '') + ' ' + (conn.medium_spec || '')).trim(); + var maxLen = Math.max(labelText.length, cableText.length); + var lineLen = Math.min(120, Math.max(50, maxLen * 6 + 20)); + var goUp = pos.isTop; + self._outputAreas.push({ + x: pos.x - 25, + y: goUp ? (pos.y - lineLen - 5) : pos.y, + w: 50, + h: lineLen + 10 + }); + } + }); + this.connections.forEach(function(conn, connIndex) { // Check is_rail as integer (PHP may return string "1" or "0") if (parseInt(conn.is_rail) === 1) { @@ -7854,6 +7901,7 @@ // Calculate line length based on label text var labelText = conn.output_label || ''; + if (conn.output_location) labelText += ' · ' + conn.output_location; 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)); @@ -7869,45 +7917,49 @@ // Draw vertical line var path = 'M ' + lineX + ' ' + startY + ' L ' + lineX + ' ' + endY; - html += ''; + // Abgänge in htmlFront (über Leitungen) + htmlFront += ''; // For bundled: draw horizontal bar connecting all terminals if (isBundled && bundleWidth > 0) { var barY = startY + (goingUp ? -5 : 5); - html += ''; + htmlFront += ''; } // Invisible hit area for clicking var hitY = goingUp ? endY : startY; var hitWidth = isBundled && bundleWidth > 0 ? Math.max(40, bundleWidth + 20) : 40; - html += ''; + htmlFront += ''; // Connection line - html += ''; + htmlFront += ''; // Arrow at end (pointing away from equipment) var arrowSize = isBundled && bundleWidth > 0 ? 8 : 5; if (goingUp) { // Arrow pointing UP - html += ''; + htmlFront += ''; } else { // Arrow pointing DOWN - html += ''; + htmlFront += ''; } // Labels - vertical text on both sides var labelY = (startY + endY) / 2; - // Left side: Bezeichnung (output_label) + // Left side: Bezeichnung (output_label) + Räumlichkeit if (conn.output_label) { - html += ''; - html += self.escapeHtml(conn.output_label); - html += ''; + htmlFront += ''; + htmlFront += self.escapeHtml(conn.output_label); + if (conn.output_location) { + htmlFront += ' · ' + self.escapeHtml(conn.output_location) + ''; + } + htmlFront += ''; } // Right side: Kabeltyp + Größe @@ -7915,23 +7967,23 @@ 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 += ''; + htmlFront += ''; + htmlFront += self.escapeHtml(cableInfo.trim()); + htmlFront += ''; } // Phase type at end of line if (conn.connection_type) { var phaseY = goingUp ? (endY - 10) : (endY + 14); - html += ''; - html += conn.connection_type; - html += ''; + htmlFront += ''; + htmlFront += conn.connection_type; + htmlFront += ''; } - html += ''; + htmlFront += ''; renderedCount++; return; } @@ -7957,18 +8009,18 @@ var junctionX = pathMatch ? parseFloat(pathMatch[1]) : 0; var junctionY = pathMatch ? parseFloat(pathMatch[2]) : 0; - html += ''; + htmlBack += ''; - html += ''; - html += ''; + htmlBack += ''; + htmlBack += ''; // Junction marker (dot at start point) if (self.displaySettings.showJunctions) { - html += ''; + htmlBack += ''; } // Label if present @@ -7983,15 +8035,15 @@ // Badge with solid background and border for visibility // Text always white for readability (e.g. L2 black wire) - html += ''; - html += ''; - html += self.escapeHtml(conn.output_label); - html += ''; + htmlBack += ''; + htmlBack += ''; + htmlBack += self.escapeHtml(conn.output_label); + htmlBack += ''; } - html += ''; + htmlBack += ''; renderedCount++; return; } @@ -8009,63 +8061,57 @@ // Farbe: gespeichert > Phase-Farbe > Fallback hellblau var inputColor = conn.color || self.PHASE_COLORS[conn.connection_type] || '#4fc3f7'; + var isTop = targetPos.isTop; - // Calculate line length based on label + // Linienlänge basierend auf 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 + // Richtung: Top-Terminal = Linie von oben, Bottom-Terminal = Linie von unten + var startY = isTop ? (targetPos.y - inputLineLength) : (targetPos.y + inputLineLength); + var path = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y; - html += ''; + // Eingänge in htmlFront (über Leitungen) + htmlFront += ''; - // Invisible hit area for clicking - html += ''; + // Invisible hit area + var hitY = isTop ? startY : targetPos.y; + htmlFront += ''; - // Connection line - html += ''; + // Verbindungslinie + htmlFront += ''; - // Circle at top (external source indicator) - html += ''; + // Kreis am externen Ende (Quell-Indikator) + htmlFront += ''; - // Arrow pointing down into terminal - html += ''; - - // Phase-Label als Badge über dem Eingang - var phaseLabel = conn.connection_type || 'L1'; - var phaseBadgeWidth = Math.max(phaseLabel.length * 9 + 12, 30); - var phaseBadgeHeight = 22; - var phaseBadgeX = targetPos.x - phaseBadgeWidth / 2; - var phaseBadgeY = startY - phaseBadgeHeight - 8; - - html += ''; - html += ''; - html += ''; + } else { + // Pfeil nach oben ins Bottom-Terminal + htmlFront += ''; } - html += ''; + // Badge am Ende der Linie: Bezeichnung wenn vorhanden, sonst Phase + var badgeLabel = conn.output_label ? self.escapeHtml(conn.output_label) : (conn.connection_type || 'L1'); + var phaseBadgeWidth = Math.max(badgeLabel.length * 9 + 12, 30); + var phaseBadgeHeight = 22; + var phaseBadgeX = targetPos.x - phaseBadgeWidth / 2; + var phaseBadgeY = isTop ? (startY - phaseBadgeHeight - 8) : (startY + 8); + + htmlFront += ''; + htmlFront += ''; + htmlBack += ''; - html += ''; - html += ''; + htmlBack += ''; + htmlBack += ''; // Junction marker (dot at end point where it connects to other wire) - html += ''; + htmlBack += ''; // Label if present if (conn.output_label) { @@ -8104,15 +8150,15 @@ var labelX = labelPos ? labelPos.x : junctionX; var labelY = labelPos ? labelPos.y : junctionY - 20; - html += ''; - html += ''; - html += self.escapeHtml(conn.output_label); - html += ''; + htmlBack += ''; + htmlBack += ''; + htmlBack += self.escapeHtml(conn.output_label); + htmlBack += ''; } - html += ''; + htmlBack += ''; renderedCount++; return; } @@ -8144,21 +8190,18 @@ path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq); } - html += ''; + htmlBack += ''; - html += ''; - html += ''; + htmlBack += ''; + htmlBack += ''; if (conn.output_label) { - // Calculate label dimensions var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120); var labelHeight = 22; - - // Find safe label position that doesn't overlap equipment var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight); if (!labelPos) { labelPos = self.getPathMidpoint(path); @@ -8166,39 +8209,35 @@ var labelX = labelPos ? labelPos.x : (sourcePos.x + targetPos.x) / 2; var labelY = labelPos ? labelPos.y : (sourcePos.y + targetPos.y) / 2; - // Badge with solid background and border for visibility - // Text always white for readability (e.g. L2 black wire) - html += ''; - html += ''; - html += self.escapeHtml(conn.output_label); - html += ''; + htmlBack += ''; + htmlBack += ''; + htmlBack += self.escapeHtml(conn.output_label); + htmlBack += ''; } if (conn.connection_type && !conn.output_label) { - // Also check for safe position for type labels var typeWidth = conn.connection_type.length * 9 + 14; var typeHeight = 18; var typePos = self.findSafeLabelPosition(path, typeWidth, typeHeight); var typeX = typePos ? typePos.x : (sourcePos.x + targetPos.x) / 2; var typeY = typePos ? typePos.y : (sourcePos.y + targetPos.y) / 2; - // Badge background for type label too - // Text always white for readability - html += ''; - html += ''; - html += conn.connection_type; - html += ''; + htmlBack += ''; + htmlBack += ''; + htmlBack += conn.connection_type; + htmlBack += ''; } - html += ''; + htmlBack += ''; renderedCount++; }); - $layer.html(html); + // Leitungen zuerst (hinten), dann Abgänge/Eingänge (vorne) + $layer.html(htmlBack + htmlFront); // Bind click events to SVG connection elements (must be done after rendering) var self = this; @@ -9686,6 +9725,17 @@ return result.top; }, + // Schutzgruppen-Farbe basierend auf Protection-Device-ID + _protectionColorCache: {}, + getProtectionColor: function(protectionId) { + if (!protectionId) return null; + if (this._protectionColorCache[protectionId]) return this._protectionColorCache[protectionId]; + var colors = ['#e74c3c', '#3498db', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4', '#ff5722']; + var idx = Object.keys(this._protectionColorCache).length % colors.length; + this._protectionColorCache[protectionId] = colors[idx]; + return colors[idx]; + }, + getTerminals: function(eq) { // Try to parse terminals_config from equipment type if (eq.terminals_config) { @@ -10009,6 +10059,17 @@ return eq; // Return the overlapping equipment } } + + // Auch gegen Abgang-Bereiche prüfen + if (this._outputAreas) { + for (var j = 0; j < this._outputAreas.length; j++) { + var oa = this._outputAreas[j]; + if (!(lx2 < oa.x || lx1 > oa.x + oa.w || ly2 < oa.y || ly1 > oa.y + oa.h)) { + return { _isOutput: true }; // Abgang-Bereich überlappt + } + } + } + return null; // No overlap }, @@ -10031,7 +10092,7 @@ if (points.length < 2) return null; - // Try positions along the path (at 10%, 30%, 50%, 70%, 90%) + // Positionen entlang des Pfades testen (Mitte bevorzugt, dann alternierend) var percentages = [0.5, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9]; // Calculate total path length and segment lengths @@ -11307,6 +11368,7 @@ } var labelText = conn.output_label || ''; + if (conn.output_location) labelText += ' · ' + conn.output_location; 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)); @@ -11314,7 +11376,7 @@ var endY = goingUp ? (sourcePos.y - lineLength) : (sourcePos.y + lineLength); pathData = 'M ' + lineX + ' ' + sourcePos.y + ' L ' + lineX + ' ' + endY; } else if (!conn.fk_source && targetEq) { - // Anschlusspunkt (Input) - Linie von oben ins Terminal + // Anschlusspunkt (Input) - Linie von außen ins Terminal var targetTerminals = this.getTerminals(targetEq); var targetTermId = conn.target_terminal_id || 't1'; var targetPos = this.getTerminalPosition(targetEq, targetTermId, targetTerminals); @@ -11322,7 +11384,7 @@ var inputLabel = conn.output_label || ''; var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30)); - var startY = targetPos.y - inputLineLength; + var startY = targetPos.isTop ? (targetPos.y - inputLineLength) : (targetPos.y + inputLineLength); pathData = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y; } @@ -12525,6 +12587,14 @@ 'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;">'; html += ''; + // Räumlichkeit + html += '
'; + html += ''; + html += ''; + html += '
'; + // Kabeltyp (from database) html += '
'; html += ''; @@ -12577,6 +12647,23 @@ }); html += '
'; + // Alle Terminals auf dieser Seite bündeln (nur bei >1 Terminal auf gleicher Seite) + var eq = self.equipment.find(function(e) { return e.id == eqId; }); + var terminals = eq ? self.getTerminals(eq) : []; + // Terminal-Seite ermitteln (top oder bottom) + var clickedTerm = terminals.find(function(t) { return t.id === termId; }); + var termSide = clickedTerm ? clickedTerm.pos : 'bottom'; + var sideTerminals = terminals.filter(function(t) { return t.pos === termSide; }); + var sideCount = sideTerminals.length; + if (sideCount > 1) { + html += '
'; + html += '
'; + } + // Buttons html += '
'; if (existingOutput) { @@ -12628,15 +12715,17 @@ }); $('.output-save-btn').on('click', function() { var label = $('.output-label').val(); + var location = $('.output-location').val(); var cableType = $('.output-cable-type').val(); var cableSpec = $('.output-cable-spec').val(); var cableLength = $('.output-length').val(); var phaseType = $('.output-phase-type').val(); + var bundled = $('.output-bundle-all').is(':checked') ? 'all' : ''; if (existingOutput) { - self.updateOutput(existingOutput.id, label, cableType, cableSpec, phaseType, cableLength); + self.updateOutput(existingOutput.id, label, location, cableType, cableSpec, phaseType, cableLength, bundled); } else { - self.createOutput(eqId, termId, label, cableType, cableSpec, phaseType, cableLength); + self.createOutput(eqId, termId, label, location, cableType, cableSpec, phaseType, cableLength, bundled); } $('.schematic-output-dialog').remove(); }); @@ -12711,7 +12800,7 @@ }, // Create a new cable output (no target, fk_target = NULL) - createOutput: function(eqId, termId, label, cableType, cableSpec, phaseType, cableLength) { + createOutput: function(eqId, termId, label, location, cableType, cableSpec, phaseType, cableLength, bundledTerminals) { var self = this; $.ajax({ @@ -12725,9 +12814,11 @@ target_terminal_id: '', connection_type: phaseType || 'L1N', output_label: label, + output_location: location || '', medium_type: cableType, medium_spec: cableSpec, medium_length: cableLength || '', + bundled_terminals: bundledTerminals || '', token: $('input[name="token"]').val() }, dataType: 'json', @@ -12746,7 +12837,7 @@ }, // Update existing output - updateOutput: function(connId, label, cableType, cableSpec, phaseType, cableLength) { + updateOutput: function(connId, label, location, cableType, cableSpec, phaseType, cableLength, bundledTerminals) { var self = this; $.ajax({ @@ -12757,9 +12848,11 @@ connection_id: connId, connection_type: phaseType || 'L1N', output_label: label, + output_location: location || '', medium_type: cableType, medium_spec: cableSpec, medium_length: cableLength || '', + bundled_terminals: bundledTerminals || '', token: $('input[name="token"]').val() }, dataType: 'json', @@ -12992,7 +13085,23 @@ var conn = this.connections.find(function(c) { return c.id == connId; }); if (!conn) return; - // Build edit dialog + // Abgang → showAbgangDialog mit existingOutput + if (conn.fk_source && !conn.fk_target && !conn.path_data) { + var eqId = conn.fk_source; + var termId = conn.source_terminal_id || 't2'; + this.showAbgangDialog(eqId, termId, window.innerWidth / 2 - 160, window.innerHeight / 2 - 150, conn); + return; + } + + // Eingang → showInputDialog mit existingInput + if (!conn.fk_source && conn.fk_target && !conn.path_data) { + var eqId = conn.fk_target; + var termId = conn.target_terminal_id || 't1'; + this.showInputDialog(eqId, termId, window.innerWidth / 2 - 140, window.innerHeight / 2 - 100, conn); + return; + } + + // Normale Verbindung → Standard-Edit-Dialog var dialogHtml = '
'; + dialogHtml += ''; + dialogHtml += '
'; + // Buttons dialogHtml += '
'; dialogHtml += '