diff --git a/CLAUDE.md b/CLAUDE.md
old mode 100644
new mode 100755
index c8fee44..4b222df
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -99,14 +99,62 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte.
## Dateistruktur
+### Tabs
- `tabs/anlagen.php` - Hauptansicht für Anlagen auf Kundenebene
- `tabs/contact_anlagen.php` - Anlagen für Kontakte
- `tabs/favoriteproducts.php` - Lieblingsprodukte auf Kundenebene
- `tabs/contact_favoriteproducts.php` - Lieblingsprodukte für Kontakte
+
+### Admin
- `admin/anlage_types.php` - Verwaltung der Element-Typen
-- `ajax/` - AJAX-Endpunkte für dynamische Funktionen
-- `js/kundenkarte.js` - Alle JavaScript-Komponenten
-- `css/kundenkarte.css` - Alle Styles (Dark Mode)
+- `admin/building_types.php` - Verwaltung der Gebäude-Typen
+- `admin/equipment_types.php` - Verwaltung der Equipment-Typen
+- `admin/setup.php` - Modul-Einstellungen
+
+### Klassen (class/)
+- `anlage.class.php` - Haupt-Anlage-Klasse
+- `anlagetype.class.php` - Element-Typen (fetchAllBySystem mit color!)
+- `buildingtype.class.php` - Gebäude-Typen
+- `anlageaccessory.class.php` - Zubehör mit CRUD + Lieferantenbestellung
+- `anlageconnection.class.php` - Kabelverbindungen (Anlagen-Ebene)
+- `anlagefile.class.php` - Datei-Anhänge
+- `anlagebackup.class.php` - Backup/Restore
+- `auditlog.class.php` - Änderungsprotokoll
+- `equipment.class.php` - Equipment-Instanzen auf Hutschienen
+- `equipmenttype.class.php` - Equipment-Typ-Vorlagen (LS, FI, Neozed etc.)
+- `equipmentcarrier.class.php` - Hutschienen (DIN-Rails)
+- `equipmentpanel.class.php` - Schaltschrankfelder (Panels)
+- `equipmentconnection.class.php` - Verbindungen im Schaltplan-Editor
+- `terminalbridge.class.php` - Terminal-Brücken
+- `mediumtype.class.php` - Leitungstypen
+- `busbartype.class.php` - Sammelschienen-Typen
+
+### 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)
+
+### AJAX-Endpunkte (ajax/) — 30+ Dateien
+- `anlage.php` - Anlagen CRUD
+- `equipment.php` - Equipment CRUD + Produkt-Suche
+- `equipment_carrier.php` - Hutschienen CRUD
+- `equipment_panel.php` - Panel CRUD
+- `equipment_connection.php` - Verbindungen CRUD
+- `anlage_accessory.php` - Zubehör CRUD + Bestellung
+- `graph_data.php` - Cytoscape Graph-Daten
+- `graph_save_positions.php` - Graph-Positionen speichern
+- `export_schematic_pdf.php` - Schaltplan PDF-Export
+- `export_wiring_diagram_pdf.php` - Leitungslaufplan PDF-Export (separates Feature)
+- `export_tree_pdf.php` - Baum PDF-Export
+- `file_preview.php` - Datei-Vorschau Tooltip
+- `pwa_api.php` - PWA-Endpoints
+
+### Frontend
+- `js/kundenkarte.js` - Haupt-JS (~11.000 Zeilen)
+- `js/kundenkarte_cytoscape.js` - Graph-JS (~900 Zeilen)
+- `js/pwa.js` - PWA-JS (~1.950 Zeilen)
+- `css/kundenkarte.css` - Alle Styles (Dark Mode Theme)
+- `css/pwa.css` - PWA-Styles
## Wichtige Hinweise
@@ -267,6 +315,71 @@ Eigene Seite für Firmen-Equipment (Werkzeuge, Maschinen, Messgeräte).
- Typ-Flag `has_accessories` steuert Verfügbarkeit
- Lieferantenbestellung via `CommandeFournisseur` generierbar
+## Terminal-Farbpropagierung (v8.6)
+
+### Übersicht
+Phasenfarben werden von den Eingängen (Anschlusspunkten) durch den gesamten Schaltplan propagiert.
+
+### Dual-Map System in JS
+- `_terminalPhaseMap` — `{eqId: {termId: "L1"}}` — Phasennamen für Busbar-Logik
+- `_terminalColorMap` — `{eqId: {termId: "#hex"}}` — Tatsächliche Hex-Farben (von `conn.color`)
+- Aufgebaut in `buildTerminalPhaseMap()` (JS Zeile ~5499)
+
+### Propagierungsreihenfolge
+1. **Inputs** (Anschlusspunkte): `conn.color` als Startfarbe, `connection_type` als Phase
+2. **Block-Durchreichung**: Top-Terminal ↔ Bottom-Terminal (paarweise)
+3. **Leitungen**: Source → Target und umgekehrt
+4. **Busbars**: Nur eingespeiste Phasen verteilen (fedPhases/fedColors)
+
+### Farbzugriff
+- `getTerminalConnectionColor(eqId, termId)` — Liest `_terminalColorMap`, Fallback auf Connection-Farben
+- Input-Labels werden als **farbige Badges** angezeigt (Phase-Name als weißer Text auf inputColor-Hintergrund)
+
+### Phasenfarben (PHASE_COLORS)
+```
+L1: '#8B4513' (braun) L2: '#1a1a1a' (schwarz) L3: '#666666' (grau)
+N: '#0066cc' (blau) PE: '#27ae60' (grün)
+```
+
+## Leitungslaufplan PDF-Export (v8.6)
+
+### Übersicht
+Normgerechter Stromlaufplan in aufgelöster Darstellung (DIN EN 61082) als PDF-Export.
+**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)
+ - `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
+
+### Abgangsnummer-Format
+`R{Reihe}.{Position}` z.B. `R1.3` = Carrier-Position 1, Equipment-TE-Position 3
+
+### Strompfad-Tracing
+Pro Abgang (Connection mit `fk_target = NULL`):
+1. Source-Equipment = LS-Schalter
+2. Phase aus `terminalPhaseMap`
+3. FI/RCD über `Equipment.fk_protection`
+4. Kabel: `medium_type` + `medium_spec` + `medium_length`
+5. Sortierung: FI-Gruppe → Carrier-Position → Equipment-Position
+
+### Phase-Map (PHP-Port)
+`WiringDiagramAnalyzer::buildPhaseMap()` ist ein 1:1 PHP-Port von JS `buildTerminalPhaseMap()`:
+- Iterativ (max 20 Durchläufe) bis keine Änderungen mehr
+- Inputs → Block-Durchreichung → Leitungen → Busbar-Verteilung
+
+### VDE-Symbole
+- LS-Schalter: Schräge Kontaktlinie + Auslöser-Rechteck
+- FI/RCD: Rechteck mit Kreis + Vertikallinie (Differenzstrom-Symbol)
+- Gezeichnet mit TCPDF-Primitiven (Line, Rect, Circle, Polygon)
+
## Select2 mit Kategorie-Filter
### Problem & Lösung
diff --git a/ajax/export_wiring_diagram_pdf.php b/ajax/export_wiring_diagram_pdf.php
new file mode 100644
index 0000000..78f9d0b
--- /dev/null
+++ b/ajax/export_wiring_diagram_pdf.php
@@ -0,0 +1,60 @@
+loadLangs(array('companies', 'kundenkarte@kundenkarte'));
+
+// Parameter
+$anlageId = GETPOSTINT('anlage_id');
+$format = GETPOST('format', 'alpha') ?: 'A3';
+$orientation = GETPOST('orientation', 'alpha') ?: 'L';
+
+// Rechte-Check
+if (!$user->hasRight('kundenkarte', 'read')) {
+ accessforbidden();
+}
+
+// Anlage laden
+$anlage = new Anlage($db);
+if ($anlage->fetch($anlageId) <= 0) {
+ die('Anlage nicht gefunden');
+}
+
+// Kunde laden
+$societe = new Societe($db);
+$societe->fetch($anlage->fk_soc);
+
+// Analyse
+$analyzer = new WiringDiagramAnalyzer($db, $anlageId);
+$analyzer->loadData();
+$analyzer->analyze();
+
+// PDF erstellen
+$pdf = pdf_getInstance();
+$pdf->SetCreator('Dolibarr - KundenKarte Leitungslaufplan');
+$pdf->SetAuthor($user->getFullName($langs));
+$pdf->SetTitle('Leitungslaufplan - '.$anlage->label);
+
+// Renderer
+$renderer = new WiringDiagramRenderer($pdf, $analyzer, $anlage, $societe, $user, $format, $orientation);
+$renderer->render();
+$renderer->renderAbgangTabelle();
+$renderer->renderLegende();
+
+// PDF ausgeben
+$filename = 'Leitungslaufplan_'.dol_sanitizeFileName($anlage->label).'_'.date('Y-m-d').'.pdf';
+$pdf->Output($filename, 'D');
diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php
index 788a876..6d527c1 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.6';
+ $this->version = '9.7';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
diff --git a/js/kundenkarte.js b/js/kundenkarte.js
index d0202ac..8364756 100644
--- a/js/kundenkarte.js
+++ b/js/kundenkarte.js
@@ -8033,10 +8033,19 @@
// Arrow pointing down into terminal
html += '';
- // Phase label at top (big, prominent)
- html += '';
- html += conn.connection_type || 'L1';
+ // 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 += phaseLabel;
html += '';
// Bezeichnung als Badge neben der Eingangsleitung
diff --git a/lib/wiring_diagram.lib.php b/lib/wiring_diagram.lib.php
new file mode 100644
index 0000000..a64c500
--- /dev/null
+++ b/lib/wiring_diagram.lib.php
@@ -0,0 +1,1239 @@
+ array(139, 69, 19), // Braun
+ 'L2' => array(26, 26, 26), // Schwarz
+ 'L3' => array(102, 102, 102), // Grau
+ 'N' => array(0, 102, 204), // Blau
+ 'PE' => array(39, 174, 96), // Grün
+ 'LN' => array(139, 69, 19), // Braun
+ '3P' => array(155, 89, 182), // Lila
+ '3P+N' => array(52, 73, 94), // Dunkelblau
+ );
+ $p = strtoupper($phase);
+ return isset($colors[$p]) ? $colors[$p] : array(136, 136, 136);
+}
+
+/**
+ * Phase-Labels parsen (PHP-Port von JS parsePhaseLabels)
+ */
+function parsePhaseLabels($phases)
+{
+ if (empty($phases)) return array();
+ $p = strtoupper(trim($phases));
+
+ $map = array(
+ '3P' => array('L1', 'L2', 'L3'),
+ 'L1L2L3' => array('L1', 'L2', 'L3'),
+ '3P+N' => array('L1', 'L2', 'L3', 'N'),
+ '3PN' => array('L1', 'L2', 'L3', 'N'),
+ '3P+N+PE' => array('L1', 'L2', 'L3', 'N', 'PE'),
+ '3PNPE' => array('L1', 'L2', 'L3', 'N', 'PE'),
+ 'L1N' => array('L1', 'N'),
+ 'L1+N' => array('L1', 'N'),
+ 'L1' => array('L1'),
+ 'L2' => array('L2'),
+ 'L3' => array('L3'),
+ 'N' => array('N'),
+ 'PE' => array('PE'),
+ );
+
+ if (isset($map[$p])) return $map[$p];
+ if (strpos($p, '+') !== false) return explode('+', $p);
+ if (strpos($p, ',') !== false) return array_map('trim', explode(',', $p));
+ return array($phases);
+}
+
+/**
+ * Terminals eines Equipment ermitteln (PHP-Port von JS getTerminals)
+ */
+function getEquipmentTerminals($eq)
+{
+ // terminals_config aus dem Equipment-Typ
+ $terminalsConfig = isset($eq->terminals_config) ? $eq->terminals_config : '';
+ if (!empty($terminalsConfig)) {
+ // Literale \r\n bereinigen
+ $configStr = str_replace(array("\\r\\n", "\\r", "\\n", "\\t"), array(' ', ' ', ' ', ''), $terminalsConfig);
+ $config = @json_decode($configStr, true);
+ if (is_array($config) && isset($config['terminals'])) {
+ return $config['terminals'];
+ }
+ if (is_array($config)) {
+ $terminals = array();
+ if (isset($config['inputs'])) {
+ foreach ($config['inputs'] as $t) {
+ $terminals[] = array('id' => $t['id'], 'label' => $t['label'] ?? '●', 'pos' => 'top');
+ }
+ }
+ if (isset($config['outputs'])) {
+ foreach ($config['outputs'] as $t) {
+ $terminals[] = array('id' => $t['id'], 'label' => $t['label'] ?? '●', 'pos' => 'bottom');
+ }
+ }
+ if (!empty($terminals)) return $terminals;
+ }
+ }
+
+ // Default-Terminals nach type_ref
+ $typeRef = strtoupper(isset($eq->type_ref) ? $eq->type_ref : '');
+ $defaults = array(
+ 'LS' => array(array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'bottom')),
+ 'FI' => array(
+ array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'),
+ array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'top'),
+ array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'),
+ array('id'=>'t7','pos'=>'bottom'), array('id'=>'t8','pos'=>'bottom'),
+ ),
+ 'FI4P' => array(
+ array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'),
+ array('id'=>'t3','pos'=>'top'), array('id'=>'t4','pos'=>'top'),
+ array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'),
+ array('id'=>'t7','pos'=>'bottom'), array('id'=>'t8','pos'=>'bottom'),
+ ),
+ 'LS3P' => array(
+ array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'top'), array('id'=>'t3','pos'=>'top'),
+ array('id'=>'t4','pos'=>'bottom'), array('id'=>'t5','pos'=>'bottom'), array('id'=>'t6','pos'=>'bottom'),
+ ),
+ 'KLEMME' => array(array('id'=>'t1','pos'=>'top'), array('id'=>'t2','pos'=>'bottom')),
+ );
+
+ if (isset($defaults[$typeRef])) return $defaults[$typeRef];
+ if (strpos($typeRef, 'FI') !== false || strpos($typeRef, 'RCD') !== false) {
+ return (strpos($typeRef, '4P') !== false) ? $defaults['FI4P'] : $defaults['FI'];
+ }
+ return $defaults['LS'];
+}
+
+
+/**
+ * WiringDiagramAnalyzer - Analysiert die Schaltplan-Daten und baut Strompfade
+ */
+class WiringDiagramAnalyzer
+{
+ private $db;
+ private $anlageId;
+
+ // Rohdaten
+ public $panels = array();
+ public $carriers = array();
+ public $allEquipment = array();
+ public $allConnections = array();
+
+ // Lookup-Maps
+ private $equipmentById = array();
+ private $carrierById = array();
+ private $carrierByEquipmentId = array();
+
+ // Phase-Propagierung
+ private $terminalPhaseMap = array();
+ private $terminalColorMap = array();
+
+ // Ergebnis
+ public $circuitPaths = array();
+
+ public function __construct($db, $anlageId)
+ {
+ $this->db = $db;
+ $this->anlageId = (int) $anlageId;
+ }
+
+ /**
+ * Alle Daten laden
+ */
+ public function loadData()
+ {
+ // Panels
+ $panelObj = new EquipmentPanel($this->db);
+ $this->panels = $panelObj->fetchByAnlage($this->anlageId);
+
+ // Carriers
+ $carrierObj = new EquipmentCarrier($this->db);
+ $this->carriers = $carrierObj->fetchByAnlage($this->anlageId);
+
+ // Equipment + Connections pro Carrier
+ $eqObj = new Equipment($this->db);
+ $connObj = new EquipmentConnection($this->db);
+
+ foreach ($this->carriers as $c) {
+ $eqList = $eqObj->fetchByCarrier($c->id);
+ $connList = $connObj->fetchByCarrier($c->id);
+ $this->allEquipment = array_merge($this->allEquipment, $eqList);
+ $this->allConnections = array_merge($this->allConnections, $connList);
+ }
+
+ // Lookup-Maps bauen
+ foreach ($this->allEquipment as $eq) {
+ $this->equipmentById[$eq->id] = $eq;
+ }
+ foreach ($this->carriers as $c) {
+ $this->carrierById[$c->id] = $c;
+ }
+ foreach ($this->allEquipment as $eq) {
+ $this->carrierByEquipmentId[$eq->id] = isset($this->carrierById[$eq->fk_carrier])
+ ? $this->carrierById[$eq->fk_carrier] : null;
+ }
+ }
+
+ /**
+ * Analyse durchführen: Phase-Map bauen + Strompfade finden
+ */
+ public function analyze()
+ {
+ $this->buildPhaseMap();
+ $this->buildCircuitPaths();
+ }
+
+ /**
+ * PHP-Port von JS buildTerminalPhaseMap()
+ * Propagiert Phasen von Eingängen durch Blöcke, Leitungen und Busbars
+ */
+ private function buildPhaseMap()
+ {
+ $this->terminalPhaseMap = array();
+ $this->terminalColorMap = array();
+
+ $validPhases = array('L1', 'L2', 'L3', 'N', 'PE');
+
+ // Hilfsfunktionen als Closures
+ $setPhase = function($eqId, $termId, $phase, $color = null) use ($validPhases) {
+ if (!isset($this->terminalPhaseMap[$eqId])) $this->terminalPhaseMap[$eqId] = array();
+ if (!isset($this->terminalColorMap[$eqId])) $this->terminalColorMap[$eqId] = array();
+ if (isset($this->terminalPhaseMap[$eqId][$termId])) return false;
+ $this->terminalPhaseMap[$eqId][$termId] = $phase;
+ $this->terminalColorMap[$eqId][$termId] = $color ?: '#888';
+ return true;
+ };
+
+ $forcePhase = function($eqId, $termId, $phase, $color = null) {
+ if (!isset($this->terminalPhaseMap[$eqId])) $this->terminalPhaseMap[$eqId] = array();
+ if (!isset($this->terminalColorMap[$eqId])) $this->terminalColorMap[$eqId] = array();
+ if (isset($this->terminalPhaseMap[$eqId][$termId]) && $this->terminalPhaseMap[$eqId][$termId] === $phase) return false;
+ $this->terminalPhaseMap[$eqId][$termId] = $phase;
+ $this->terminalColorMap[$eqId][$termId] = $color ?: '#888';
+ return true;
+ };
+
+ $getColor = function($eqId, $termId) {
+ return isset($this->terminalColorMap[$eqId][$termId]) ? $this->terminalColorMap[$eqId][$termId] : null;
+ };
+
+ // Schritt 1: Anschlusspunkte (Inputs) als Startpunkte
+ foreach ($this->allConnections as $conn) {
+ if ($conn->is_rail) continue;
+ if (!empty($conn->fk_source)) continue;
+ if (empty($conn->fk_target) || empty($conn->target_terminal_id)) continue;
+
+ $phase = strtoupper($conn->connection_type ?: '');
+ if (!in_array($phase, $validPhases)) continue;
+
+ $rgb = getPhaseColorRGB($phase);
+ $inputColor = !empty($conn->color) ? $conn->color : sprintf('#%02x%02x%02x', $rgb[0], $rgb[1], $rgb[2]);
+ $setPhase($conn->fk_target, $conn->target_terminal_id, $phase, $inputColor);
+ }
+
+ // Schritt 2: Iterativ propagieren
+ $changed = true;
+ $iterations = 0;
+ while ($changed && $iterations++ < 20) {
+ $changed = false;
+
+ // Block-Durchreichung (top ↔ bottom)
+ foreach ($this->allEquipment as $eq) {
+ $terminals = getEquipmentTerminals($eq);
+ $topTerminals = array_values(array_filter($terminals, function($t) { return ($t['pos'] ?? '') === 'top'; }));
+ $bottomTerminals = array_values(array_filter($terminals, function($t) { return ($t['pos'] ?? '') === 'bottom'; }));
+
+ $pairCount = min(count($topTerminals), count($bottomTerminals));
+ for ($i = 0; $i < $pairCount; $i++) {
+ $topId = $topTerminals[$i]['id'] ?? 't'.($i+1);
+ $botId = $bottomTerminals[$i]['id'] ?? 't'.($i + count($topTerminals) + 1);
+ $topPhase = $this->terminalPhaseMap[$eq->id][$topId] ?? null;
+ $botPhase = $this->terminalPhaseMap[$eq->id][$botId] ?? null;
+
+ if ($topPhase && !$botPhase) {
+ if ($setPhase($eq->id, $botId, $topPhase, $getColor($eq->id, $topId))) $changed = true;
+ } elseif ($botPhase && !$topPhase) {
+ if ($setPhase($eq->id, $topId, $botPhase, $getColor($eq->id, $botId))) $changed = true;
+ }
+ }
+ }
+
+ // Leitungen propagieren
+ foreach ($this->allConnections as $conn) {
+ if ($conn->is_rail) continue;
+ if (empty($conn->fk_source) || empty($conn->fk_target)) continue;
+ if (empty($conn->source_terminal_id) || empty($conn->target_terminal_id)) continue;
+
+ $srcPhase = $this->terminalPhaseMap[$conn->fk_source][$conn->source_terminal_id] ?? null;
+ $tgtPhase = $this->terminalPhaseMap[$conn->fk_target][$conn->target_terminal_id] ?? null;
+
+ if ($srcPhase && !$tgtPhase) {
+ if ($setPhase($conn->fk_target, $conn->target_terminal_id, $srcPhase, $getColor($conn->fk_source, $conn->source_terminal_id))) $changed = true;
+ } elseif ($tgtPhase && !$srcPhase) {
+ if ($setPhase($conn->fk_source, $conn->source_terminal_id, $tgtPhase, $getColor($conn->fk_target, $conn->target_terminal_id))) $changed = true;
+ }
+ }
+
+ // Busbar-Verteilung
+ foreach ($this->allConnections as $busbar) {
+ if (!$busbar->is_rail) continue;
+
+ $railStart = (int) ($busbar->rail_start_te ?: 1);
+ $railEnd = (int) ($busbar->rail_end_te ?: $railStart);
+ $posY = (int) ($busbar->position_y ?: 0);
+ $targetPos = ($posY === 0) ? 'top' : 'bottom';
+
+ // Phase-Labels
+ $phaseLabels = array();
+ if (!empty($busbar->phases_config)) {
+ $pc = @json_decode($busbar->phases_config, true);
+ if (is_array($pc) && !empty($pc)) $phaseLabels = $pc;
+ }
+ if (empty($phaseLabels)) {
+ $phaseLabels = parsePhaseLabels($busbar->rail_phases ?: $busbar->connection_type ?: '');
+ }
+ if (empty($phaseLabels)) continue;
+
+ // Eingespeiste Phasen sammeln
+ $fedPhases = array();
+ $fedColors = array();
+ foreach ($this->allEquipment as $eq) {
+ if ($eq->fk_carrier != $busbar->fk_carrier) continue;
+ $eqPosTE = floatval($eq->position_te ?: 1);
+ $eqWidthTE = floatval($eq->width_te ?: 1);
+ if (!($eqPosTE < $railEnd + 1 && $railStart < $eqPosTE + $eqWidthTE)) continue;
+
+ $terminals = getEquipmentTerminals($eq);
+ foreach ($terminals as $term) {
+ if (($term['pos'] ?? '') !== $targetPos) continue;
+ $termId = $term['id'] ?? '';
+ $phase = $this->terminalPhaseMap[$eq->id][$termId] ?? null;
+ if ($phase) {
+ $fedPhases[$phase] = true;
+ if (!isset($fedColors[$phase])) {
+ $fedColors[$phase] = $getColor($eq->id, $termId);
+ }
+ }
+ }
+ }
+
+ if (empty($fedPhases)) continue;
+
+ // Excluded TEs
+ $excludedTEs = array();
+ if (!empty($busbar->excluded_te)) {
+ $excludedTEs = array_map('intval', array_filter(array_map('trim', explode(',', $busbar->excluded_te))));
+ }
+
+ // Verteilen
+ foreach ($this->allEquipment as $eq) {
+ if ($eq->fk_carrier != $busbar->fk_carrier) continue;
+ $eqPosTE = floatval($eq->position_te ?: 1);
+ $eqWidthTE = floatval($eq->width_te ?: 1);
+ if (!($eqPosTE < $railEnd + 1 && $railStart < $eqPosTE + $eqWidthTE)) continue;
+
+ $terminals = getEquipmentTerminals($eq);
+ $posTerminals = array_values(array_filter($terminals, function($t) use ($targetPos) { return ($t['pos'] ?? '') === $targetPos; }));
+
+ foreach ($posTerminals as $idx => $term) {
+ $col = isset($term['col']) ? $term['col'] : ($idx % max(1, $eqWidthTE));
+ $absoluteTE = round($eqPosTE + $col);
+
+ if (in_array($absoluteTE, $excludedTEs)) continue;
+ if ($absoluteTE < $railStart || $absoluteTE > $railEnd) continue;
+
+ $teOffset = $absoluteTE - $railStart;
+ $phase = $phaseLabels[$teOffset % count($phaseLabels)];
+
+ if (!isset($fedPhases[$phase])) continue;
+
+ $phaseColor = $fedColors[$phase] ?? '#888';
+ $termId = $term['id'] ?? 't'.($idx+1);
+ if ($forcePhase($eq->id, $termId, $phase, $phaseColor)) $changed = true;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Strompfade bauen: Für jeden Abgang eine Spalte
+ */
+ private function buildCircuitPaths()
+ {
+ $this->circuitPaths = array();
+
+ // Alle Abgänge (Outputs) finden
+ foreach ($this->allConnections as $conn) {
+ if ($conn->is_rail) continue;
+ // Abgang = hat Source, kein Target
+ if (empty($conn->fk_source) || !empty($conn->fk_target)) continue;
+ // path_data = Junction-Verbindung, kein echter Abgang
+ if (!empty($conn->path_data)) continue;
+
+ $sourceEq = $this->equipmentById[$conn->fk_source] ?? null;
+ if (!$sourceEq) continue;
+
+ $carrier = $this->carrierByEquipmentId[$conn->fk_source] ?? null;
+ if (!$carrier) continue;
+
+ // Panel ermitteln
+ $panel = null;
+ if (!empty($carrier->fk_panel)) {
+ foreach ($this->panels as $p) {
+ if ($p->id == $carrier->fk_panel) { $panel = $p; break; }
+ }
+ }
+
+ // Phase bestimmen
+ $phase = '';
+ if (!empty($conn->connection_type) && in_array(strtoupper($conn->connection_type), array('L1','L2','L3','N','PE','LN','3P','3P+N'))) {
+ $phase = strtoupper($conn->connection_type);
+ }
+ // Fallback: Aus Phase-Map
+ if (empty($phase) && !empty($conn->source_terminal_id)) {
+ $phase = $this->terminalPhaseMap[$conn->fk_source][$conn->source_terminal_id] ?? '';
+ }
+ // Fallback: Erstbeste Phase vom Equipment
+ if (empty($phase) && isset($this->terminalPhaseMap[$conn->fk_source])) {
+ $phases = array_values($this->terminalPhaseMap[$conn->fk_source]);
+ if (!empty($phases)) $phase = $phases[0];
+ }
+
+ // Schutzgerät (FI/RCD)
+ $protectionDevice = null;
+ if (!empty($sourceEq->fk_protection)) {
+ $protectionDevice = $this->equipmentById[$sourceEq->fk_protection] ?? null;
+ }
+
+ // Block-Label (z.B. "B16", "C32")
+ $blockLabel = $sourceEq->getBlockLabel();
+
+ // Abgangsnummer: R{Reihe}.{Position}
+ $reihe = ($carrier->position ?? 0) + 1;
+ $pos = round(floatval($sourceEq->position_te));
+ $abgangNr = 'R'.$reihe.'.'.$pos;
+
+ // Kette aufbauen (von oben nach unten)
+ $chain = array();
+
+ // Phase-Rail
+ $chain[] = array('type' => 'phase_rail', 'label' => $phase ?: '?');
+
+ // Schutzgerät
+ if ($protectionDevice) {
+ $protLabel = $protectionDevice->label ?: ($protectionDevice->type_label_short ?: 'FI');
+ $protBlock = $protectionDevice->getBlockLabel();
+ $chain[] = array(
+ 'type' => 'protection',
+ 'equipment' => $protectionDevice,
+ 'label' => $protLabel,
+ 'block_label' => $protBlock,
+ );
+ }
+
+ // LS-Schalter (Breaker)
+ $chain[] = array(
+ 'type' => 'breaker',
+ 'equipment' => $sourceEq,
+ 'label' => $sourceEq->label ?: ($sourceEq->type_label_short ?: 'LS'),
+ 'block_label' => $blockLabel,
+ );
+
+ // Verbraucher (Abgang)
+ $chain[] = array('type' => 'consumer', 'label' => $conn->output_label ?: '-');
+
+ $this->circuitPaths[] = array(
+ 'abgang_nr' => $abgangNr,
+ 'output_label' => $conn->output_label ?: '-',
+ 'phase' => $phase ?: '?',
+ 'phase_color_rgb' => getPhaseColorRGB($phase),
+ 'medium_type' => $conn->medium_type ?: '',
+ 'medium_spec' => $conn->medium_spec ?: '',
+ 'medium_length' => $conn->medium_length ?: '',
+ 'chain' => $chain,
+ 'protection_device' => $protectionDevice,
+ 'breaker' => $sourceEq,
+ 'carrier' => $carrier,
+ 'panel' => $panel,
+ 'connection' => $conn,
+ );
+ }
+
+ // Sortierung: FI-Gruppe → Carrier → Position
+ usort($this->circuitPaths, function($a, $b) {
+ $protA = $a['protection_device'] ? $a['protection_device']->id : PHP_INT_MAX;
+ $protB = $b['protection_device'] ? $b['protection_device']->id : PHP_INT_MAX;
+ if ($protA !== $protB) return $protA - $protB;
+
+ $carrA = $a['carrier']->position ?? 0;
+ $carrB = $b['carrier']->position ?? 0;
+ if ($carrA !== $carrB) return $carrA - $carrB;
+
+ $posA = $a['breaker']->position_te ?? 0;
+ $posB = $b['breaker']->position_te ?? 0;
+ return $posA <=> $posB;
+ });
+ }
+
+ /**
+ * Ergebnis: Strompfade
+ */
+ public function getCircuitPaths()
+ {
+ return $this->circuitPaths;
+ }
+
+ /**
+ * Abgangs-Tabelle pro Carrier/Panel
+ */
+ public function getAbgangTabelle()
+ {
+ $tabellen = array();
+
+ foreach ($this->carriers as $carrier) {
+ $panel = null;
+ if (!empty($carrier->fk_panel)) {
+ foreach ($this->panels as $p) {
+ if ($p->id == $carrier->fk_panel) { $panel = $p; break; }
+ }
+ }
+
+ $header = '';
+ if ($panel) $header .= $panel->label . ', ';
+ $header .= $carrier->label ?: ('Reihe '.($carrier->position + 1));
+
+ $rows = array();
+ foreach ($this->circuitPaths as $path) {
+ if ($path['carrier']->id != $carrier->id) continue;
+
+ $protLabel = '';
+ if ($path['protection_device']) {
+ $pd = $path['protection_device'];
+ $protLabel = ($pd->label ?: $pd->type_label_short ?: 'FI');
+ $protBlock = $pd->getBlockLabel();
+ if ($protBlock) $protLabel .= ' '.$protBlock;
+ }
+
+ $kabel = $path['medium_type'];
+ if ($path['medium_spec']) $kabel .= ' '.$path['medium_spec'];
+ if ($path['medium_length']) $kabel .= ' ('.$path['medium_length'].')';
+
+ $rows[] = array(
+ 'abgang_nr' => $path['abgang_nr'],
+ 'bezeichnung' => $path['output_label'],
+ 'phase' => $path['phase'],
+ 'absicherung' => $path['chain'][count($path['chain'])-2]['block_label'] ?? '',
+ 'kabel' => trim($kabel),
+ 'schutzgeraet' => $protLabel,
+ 'bemerkung' => '',
+ );
+ }
+
+ if (!empty($rows)) {
+ $tabellen[] = array(
+ 'header' => $header,
+ 'carrier' => $carrier,
+ 'panel' => $panel,
+ 'rows' => $rows,
+ );
+ }
+ }
+
+ return $tabellen;
+ }
+}
+
+
+/**
+ * WiringDiagramRenderer - Zeichnet den Leitungslaufplan als PDF
+ */
+class WiringDiagramRenderer
+{
+ private $pdf;
+ private $circuitPaths;
+ private $analyzer;
+ private $anlage;
+ private $societe;
+ private $user;
+
+ // Seitengröße
+ private $pageWidth;
+ private $pageHeight;
+ private $orientation;
+ private $format;
+
+ // Layout-Konstanten
+ const MARGIN_LEFT = 15;
+ const MARGIN_RIGHT = 15;
+ const MARGIN_TOP = 12;
+ const COLUMN_WIDTH = 25;
+ const COLUMN_GAP = 3;
+ const PHASE_GAP = 5;
+
+ // Vertikale Positionen (werden in calculateLayout berechnet)
+ private $yPhaseL1;
+ private $yPhaseL2;
+ private $yPhaseL3;
+ private $yFiTop;
+ private $yFiBottom;
+ private $yLsTop;
+ private $yLsBottom;
+ private $yConsumer;
+ private $yCableLabel;
+ private $yAbgangLabel;
+ private $yAbgangNr;
+ private $yNRail;
+ private $yPeRail;
+ private $maxColumnsPerPage;
+
+ private $currentPage = 0;
+ private $totalPages = 1;
+
+ public function __construct($pdf, $analyzer, $anlage, $societe, $user, $format = 'A3', $orientation = 'L')
+ {
+ $this->pdf = $pdf;
+ $this->analyzer = $analyzer;
+ $this->circuitPaths = $analyzer->getCircuitPaths();
+ $this->anlage = $anlage;
+ $this->societe = $societe;
+ $this->user = $user;
+ $this->format = $format;
+ $this->orientation = $orientation;
+
+ // Seitengröße
+ if ($format == 'A3') {
+ $this->pageWidth = 420;
+ $this->pageHeight = 297;
+ } else {
+ $this->pageWidth = 297;
+ $this->pageHeight = 210;
+ }
+ if ($orientation == 'P') {
+ $tmp = $this->pageWidth;
+ $this->pageWidth = $this->pageHeight;
+ $this->pageHeight = $tmp;
+ }
+
+ $this->calculateLayout();
+ }
+
+ /**
+ * Layout berechnen
+ */
+ private function calculateLayout()
+ {
+ $this->yPhaseL1 = self::MARGIN_TOP + 15;
+ $this->yPhaseL2 = $this->yPhaseL1 + self::PHASE_GAP;
+ $this->yPhaseL3 = $this->yPhaseL2 + self::PHASE_GAP;
+
+ $this->yFiTop = $this->yPhaseL3 + 18;
+ $this->yFiBottom = $this->yFiTop + 22;
+
+ $this->yLsTop = $this->yFiBottom + 12;
+ $this->yLsBottom = $this->yLsTop + 18;
+
+ $this->yConsumer = $this->yLsBottom + 12;
+ $this->yCableLabel = $this->yConsumer + 10;
+ $this->yAbgangLabel = $this->yCableLabel + 10;
+ $this->yAbgangNr = $this->yAbgangLabel + 8;
+
+ // N und PE unten (vor Titelfeld)
+ $this->yNRail = $this->pageHeight - 85;
+ $this->yPeRail = $this->yNRail + self::PHASE_GAP;
+
+ // Max Spalten pro Seite
+ $usableWidth = $this->pageWidth - self::MARGIN_LEFT - self::MARGIN_RIGHT - 30; // 30mm Phase-Labels links
+ $this->maxColumnsPerPage = max(1, floor($usableWidth / (self::COLUMN_WIDTH + self::COLUMN_GAP)));
+
+ // Gesamtseiten berechnen
+ $totalPaths = count($this->circuitPaths);
+ $this->totalPages = max(1, ceil($totalPaths / $this->maxColumnsPerPage));
+ }
+
+ /**
+ * Leitungslaufplan zeichnen (alle Seiten)
+ */
+ public function render()
+ {
+ $totalPaths = count($this->circuitPaths);
+
+ if ($totalPaths === 0) {
+ $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight));
+ $this->pdf->SetFont('dejavusans', 'B', 14);
+ $this->pdf->SetTextColor(100, 100, 100);
+ $this->pdf->Text(self::MARGIN_LEFT, $this->pageHeight / 2, 'Keine Abgänge konfiguriert');
+ $this->drawTitleBlock(1, 1);
+ return;
+ }
+
+ $pathIndex = 0;
+ $pageNum = 0;
+
+ while ($pathIndex < $totalPaths) {
+ $pageNum++;
+ $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight));
+
+ // Spalten für diese Seite
+ $pagePaths = array_slice($this->circuitPaths, $pathIndex, $this->maxColumnsPerPage);
+ $numCols = count($pagePaths);
+
+ $startX = self::MARGIN_LEFT + 30; // 30mm für Phase-Labels links
+ $endX = $startX + ($numCols * (self::COLUMN_WIDTH + self::COLUMN_GAP));
+
+ // Phase-Labels links
+ $this->drawPhaseLabels();
+
+ // Phasenleiter oben (L1, L2, L3)
+ $this->drawPhaseRails($startX - 5, $endX + 5);
+
+ // N und PE unten
+ $this->drawNPeRails($startX - 5, $endX + 5);
+
+ // FI-Gruppen identifizieren für Trennlinien
+ $lastProtId = null;
+
+ // Strompfad-Spalten zeichnen
+ for ($col = 0; $col < $numCols; $col++) {
+ $path = $pagePaths[$col];
+ $x = $startX + $col * (self::COLUMN_WIDTH + self::COLUMN_GAP) + self::COLUMN_WIDTH / 2;
+
+ // FI-Gruppen-Trenner
+ $currentProtId = $path['protection_device'] ? $path['protection_device']->id : 0;
+ if ($lastProtId !== null && $lastProtId !== $currentProtId && $col > 0) {
+ $sepX = $x - (self::COLUMN_WIDTH + self::COLUMN_GAP) / 2;
+ $this->pdf->SetDrawColor(180, 180, 180);
+ $this->pdf->SetLineWidth(0.2);
+ $this->pdf->SetLineDashPattern(array(2, 2));
+ $this->pdf->Line($sepX, $this->yPhaseL1 - 5, $sepX, $this->yPeRail + 5);
+ $this->pdf->SetLineDashPattern(array());
+ }
+ $lastProtId = $currentProtId;
+
+ $this->drawCircuitColumn($path, $x);
+ }
+
+ // Titelfeld
+ $this->drawTitleBlock($pageNum, $this->totalPages + 2); // +2 für Tabelle + Legende
+
+ $pathIndex += $numCols;
+ }
+ }
+
+ /**
+ * Phase-Labels links zeichnen
+ */
+ private function drawPhaseLabels()
+ {
+ $x = self::MARGIN_LEFT;
+ $this->pdf->SetFont('dejavusans', 'B', 9);
+
+ // L1
+ $rgb = getPhaseColorRGB('L1');
+ $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Text($x, $this->yPhaseL1 - 1, 'L1');
+
+ // L2
+ $rgb = getPhaseColorRGB('L2');
+ $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Text($x, $this->yPhaseL2 - 1, 'L2');
+
+ // L3
+ $rgb = getPhaseColorRGB('L3');
+ $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Text($x, $this->yPhaseL3 - 1, 'L3');
+
+ // N
+ $rgb = getPhaseColorRGB('N');
+ $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Text($x, $this->yNRail - 1, 'N');
+
+ // PE
+ $rgb = getPhaseColorRGB('PE');
+ $this->pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Text($x, $this->yPeRail - 1, 'PE');
+
+ $this->pdf->SetTextColor(0, 0, 0);
+ }
+
+ /**
+ * Horizontale Phasenleiter oben (L1, L2, L3)
+ */
+ private function drawPhaseRails($startX, $endX)
+ {
+ $this->pdf->SetLineWidth(0.5);
+
+ $phases = array('L1' => $this->yPhaseL1, 'L2' => $this->yPhaseL2, 'L3' => $this->yPhaseL3);
+ foreach ($phases as $phase => $y) {
+ $rgb = getPhaseColorRGB($phase);
+ $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Line($startX, $y, $endX, $y);
+ }
+ }
+
+ /**
+ * N und PE Leiter unten
+ */
+ private function drawNPeRails($startX, $endX)
+ {
+ $this->pdf->SetLineWidth(0.5);
+
+ $rgb = getPhaseColorRGB('N');
+ $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Line($startX, $this->yNRail, $endX, $this->yNRail);
+
+ $rgb = getPhaseColorRGB('PE');
+ $this->pdf->SetDrawColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Line($startX, $this->yPeRail, $endX, $this->yPeRail);
+ }
+
+ /**
+ * Eine Strompfad-Spalte zeichnen
+ */
+ private function drawCircuitColumn($path, $x)
+ {
+ $phase = $path['phase'];
+ $phaseRGB = $path['phase_color_rgb'];
+
+ // Y-Position der Phase bestimmen (L1, L2 oder L3)
+ $phaseY = $this->yPhaseL1;
+ if ($phase === 'L2') $phaseY = $this->yPhaseL2;
+ elseif ($phase === 'L3') $phaseY = $this->yPhaseL3;
+
+ $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]);
+ $this->pdf->SetLineWidth(0.4);
+
+ // Vertikale Linie von Phase runter zum FI oder LS
+ $hasFI = !empty($path['protection_device']);
+ $topTarget = $hasFI ? $this->yFiTop : $this->yLsTop;
+
+ // Anschluss an Phase-Rail (kleiner Punkt)
+ $this->pdf->Circle($x, $phaseY, 1, 0, 360, 'F', array(), array($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]));
+
+ // Vertikale Linie Phase → FI/LS
+ $this->pdf->Line($x, $phaseY, $x, $topTarget);
+
+ // FI/RCD zeichnen
+ if ($hasFI) {
+ $this->drawRCDSymbol($x, $this->yFiTop, $path['protection_device']);
+
+ // Linie FI → LS
+ $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]);
+ $this->pdf->SetLineWidth(0.4);
+ $this->pdf->Line($x, $this->yFiBottom, $x, $this->yLsTop);
+ }
+
+ // LS-Schalter zeichnen
+ $this->drawBreakerSymbol($x, $this->yLsTop, $path['breaker'], $path['chain']);
+
+ // Linie LS → Verbraucher
+ $this->pdf->SetDrawColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]);
+ $this->pdf->SetLineWidth(0.4);
+ $this->pdf->Line($x, $this->yLsBottom, $x, $this->yConsumer);
+
+ // Abgang-Pfeil
+ $this->pdf->SetFillColor($phaseRGB[0], $phaseRGB[1], $phaseRGB[2]);
+ $arrowSize = 3;
+ $this->pdf->Polygon(array(
+ $x - $arrowSize, $this->yConsumer - $arrowSize,
+ $x + $arrowSize, $this->yConsumer - $arrowSize,
+ $x, $this->yConsumer + 1,
+ ), 'F');
+
+ // Vertikale Linie zum N-Leiter
+ $this->pdf->SetDrawColor(180, 180, 180);
+ $this->pdf->SetLineWidth(0.15);
+ $this->pdf->SetLineDashPattern(array(1, 2));
+ $this->pdf->Line($x, $this->yConsumer + 2, $x, $this->yNRail);
+ $this->pdf->SetLineDashPattern(array());
+
+ // Anschluss an N-Rail
+ $nRGB = getPhaseColorRGB('N');
+ $this->pdf->Circle($x, $this->yNRail, 0.8, 0, 360, 'F', array(), array($nRGB[0], $nRGB[1], $nRGB[2]));
+
+ // Kabelbezeichnung
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetTextColor(100, 100, 100);
+ $cableText = $path['medium_type'];
+ if ($path['medium_spec']) $cableText .= "\n".$path['medium_spec'];
+ if (!empty($cableText)) {
+ $this->pdf->SetXY($x - 12, $this->yCableLabel);
+ $this->pdf->MultiCell(24, 3, $cableText, 0, 'C');
+ }
+
+ // Abgang-Label (Verbraucher-Name)
+ $this->pdf->SetFont('dejavusans', 'B', 7);
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->SetXY($x - 13, $this->yAbgangLabel);
+ $this->pdf->MultiCell(26, 3, $path['output_label'], 0, 'C');
+
+ // Abgangsnummer
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetTextColor(120, 120, 120);
+ $this->pdf->Text($x - 6, $this->yAbgangNr + 8, $path['abgang_nr']);
+ }
+
+ /**
+ * LS-Schalter Symbol (vereinfacht)
+ */
+ private function drawBreakerSymbol($x, $y, $eq, $chain)
+ {
+ $this->pdf->SetDrawColor(0, 0, 0);
+ $this->pdf->SetLineWidth(0.3);
+
+ // Vertikale Linie oben
+ $this->pdf->Line($x, $y, $x, $y + 4);
+
+ // Schaltkontakt (schräge Linie)
+ $this->pdf->Line($x, $y + 4, $x + 4, $y + 8);
+
+ // Auslöser (kleines Rechteck)
+ $this->pdf->SetFillColor(255, 255, 255);
+ $this->pdf->Rect($x - 2, $y + 8, 4, 3, 'DF');
+
+ // Vertikale Linie unten
+ $this->pdf->Line($x, $y + 11, $x, $y + 18);
+
+ // Block-Label rechts (z.B. "B16")
+ $blockLabel = '';
+ foreach ($chain as $c) {
+ if ($c['type'] === 'breaker') {
+ $blockLabel = $c['block_label'] ?? '';
+ break;
+ }
+ }
+ $this->pdf->SetFont('dejavusans', 'B', 7);
+ $this->pdf->SetTextColor(0, 0, 0);
+ if ($blockLabel) {
+ $this->pdf->Text($x + 5, $y + 7, $blockLabel);
+ }
+
+ // Equipment-Label links (z.B. "F1")
+ $label = $eq->label ?: '';
+ if ($label) {
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetTextColor(80, 80, 80);
+ $this->pdf->Text($x - 12, $y + 7, $label);
+ }
+ }
+
+ /**
+ * FI/RCD Symbol (vereinfacht)
+ */
+ private function drawRCDSymbol($x, $y, $eq)
+ {
+ $this->pdf->SetDrawColor(0, 0, 0);
+ $this->pdf->SetLineWidth(0.3);
+
+ $w = 16;
+ $h = 20;
+
+ // Umrandung
+ $this->pdf->SetFillColor(255, 255, 255);
+ $this->pdf->Rect($x - $w/2, $y, $w, $h, 'DF');
+
+ // Differenzstrom-Symbol (Kreis)
+ $this->pdf->Circle($x, $y + $h/2, 4, 0, 360, 'D');
+
+ // Vertikale Linie durch Kreis (Auslöser)
+ $this->pdf->Line($x, $y + $h/2 - 4, $x, $y + $h/2 + 4);
+
+ // Block-Label (z.B. "40A 30mA")
+ $blockLabel = $eq->getBlockLabel();
+ $this->pdf->SetFont('dejavusans', '', 5);
+ $this->pdf->SetTextColor(0, 0, 0);
+ if ($blockLabel) {
+ $this->pdf->SetXY($x - $w/2, $y + $h - 5);
+ $this->pdf->Cell($w, 4, $blockLabel, 0, 0, 'C');
+ }
+
+ // Equipment-Label links (z.B. "Q1")
+ $label = $eq->label ?: '';
+ if ($label) {
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetTextColor(80, 80, 80);
+ $this->pdf->Text($x - $w/2 - 10, $y + $h/2, $label);
+ }
+
+ // Ein/Ausgangs-Linien
+ $this->pdf->SetDrawColor(0, 0, 0);
+ $this->pdf->Line($x, $y - 2, $x, $y);
+ $this->pdf->Line($x, $y + $h, $x, $y + $h + 2);
+ }
+
+ /**
+ * Abgangs-Tabellen zeichnen
+ */
+ public function renderAbgangTabelle()
+ {
+ $tabellen = $this->analyzer->getAbgangTabelle();
+ if (empty($tabellen)) return;
+
+ // Spaltenbreiten
+ $colWidths = array(18, 50, 15, 28, 45, 40, 30);
+ $colHeaders = array('Abg.Nr.', 'Bezeichnung', 'Phase', 'Absicherung', 'Kabel', 'Schutzgerät', 'Bemerkung');
+ $totalWidth = array_sum($colWidths);
+
+ foreach ($tabellen as $tabelle) {
+ $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight));
+
+ $y = self::MARGIN_TOP;
+
+ // Tabellen-Header
+ $this->pdf->SetFont('dejavusans', 'B', 12);
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text(self::MARGIN_LEFT, $y + 5, 'Abgangsverzeichnis - '.$tabelle['header']);
+ $y += 15;
+
+ // Spalten-Header
+ $this->pdf->SetFont('dejavusans', 'B', 7);
+ $this->pdf->SetFillColor(230, 230, 230);
+ $x = self::MARGIN_LEFT;
+ for ($i = 0; $i < count($colHeaders); $i++) {
+ $this->pdf->SetXY($x, $y);
+ $this->pdf->Cell($colWidths[$i], 7, $colHeaders[$i], 1, 0, 'C', true);
+ $x += $colWidths[$i];
+ }
+ $y += 7;
+
+ // Zeilen
+ $this->pdf->SetFont('dejavusans', '', 7);
+ $this->pdf->SetFillColor(255, 255, 255);
+
+ foreach ($tabelle['rows'] as $rowIdx => $row) {
+ $bgFill = ($rowIdx % 2 === 0);
+ if ($bgFill) $this->pdf->SetFillColor(248, 248, 248);
+ else $this->pdf->SetFillColor(255, 255, 255);
+
+ $x = self::MARGIN_LEFT;
+ $cells = array(
+ $row['abgang_nr'],
+ $row['bezeichnung'],
+ $row['phase'],
+ $row['absicherung'],
+ $row['kabel'],
+ $row['schutzgeraet'],
+ $row['bemerkung'],
+ );
+
+ for ($i = 0; $i < count($cells); $i++) {
+ $this->pdf->SetXY($x, $y);
+ $align = ($i === 2) ? 'C' : 'L'; // Phase zentriert
+ $this->pdf->Cell($colWidths[$i], 6, $cells[$i], 1, 0, $align, true);
+ $x += $colWidths[$i];
+ }
+ $y += 6;
+
+ // Seitenumbruch
+ if ($y > $this->pageHeight - 30) {
+ $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight));
+ $y = self::MARGIN_TOP + 10;
+ }
+ }
+
+ // Titelfeld
+ $this->drawTitleBlock(0, 0);
+ }
+ }
+
+ /**
+ * Legende zeichnen
+ */
+ public function renderLegende()
+ {
+ $this->pdf->AddPage($this->orientation, array($this->pageWidth, $this->pageHeight));
+
+ $y = self::MARGIN_TOP;
+ $x = self::MARGIN_LEFT;
+
+ $this->pdf->SetFont('dejavusans', 'B', 14);
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text($x, $y + 5, 'Legende');
+ $y += 15;
+
+ // Phasenfarben
+ $this->pdf->SetFont('dejavusans', 'B', 10);
+ $this->pdf->Text($x, $y, 'Phasenfarben nach DIN VDE');
+ $y += 8;
+
+ $phases = array(
+ 'L1' => 'Außenleiter 1 (Braun)',
+ 'L2' => 'Außenleiter 2 (Schwarz)',
+ 'L3' => 'Außenleiter 3 (Grau)',
+ 'N' => 'Neutralleiter (Blau)',
+ 'PE' => 'Schutzleiter (Grün-Gelb)',
+ );
+
+ $this->pdf->SetFont('dejavusans', '', 8);
+ foreach ($phases as $phase => $label) {
+ $rgb = getPhaseColorRGB($phase);
+ $this->pdf->SetFillColor($rgb[0], $rgb[1], $rgb[2]);
+ $this->pdf->Rect($x, $y, 20, 5, 'F');
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text($x + 25, $y + 3, $phase.' - '.$label);
+ $y += 8;
+ }
+
+ $y += 5;
+
+ // Symbole
+ $this->pdf->SetFont('dejavusans', 'B', 10);
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text($x, $y, 'Symbole');
+ $y += 10;
+
+ // LS-Symbol + Beschreibung
+ $this->pdf->SetFont('dejavusans', '', 8);
+ $this->drawBreakerSymbol($x + 8, $y, (object)array('label'=>'F1'), array(array('type'=>'breaker','block_label'=>'B16')));
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text($x + 30, $y + 8, 'Leitungsschutzschalter (LS-Schalter)');
+ $y += 25;
+
+ // FI-Symbol + Beschreibung
+ $protDummy = new stdClass();
+ $protDummy->label = 'Q1';
+ $protDummy->type_label_short = 'FI';
+ // getBlockLabel brauchen wir nicht, Text wird direkt gesetzt
+ $this->pdf->SetDrawColor(0, 0, 0);
+ $this->pdf->SetLineWidth(0.3);
+ $this->pdf->SetFillColor(255, 255, 255);
+ $this->pdf->Rect($x, $y, 16, 20, 'DF');
+ $this->pdf->Circle($x + 8, $y + 10, 4, 0, 360, 'D');
+ $this->pdf->Line($x + 8, $y + 6, $x + 8, $y + 14);
+ $this->pdf->SetFont('dejavusans', '', 5);
+ $this->pdf->Text($x + 2, $y + 17, 'FI/RCD');
+ $this->pdf->SetFont('dejavusans', '', 8);
+ $this->pdf->SetTextColor(0, 0, 0);
+ $this->pdf->Text($x + 30, $y + 10, 'Fehlerstrom-Schutzschalter (FI/RCD)');
+ $y += 28;
+
+ // Abgang-Pfeil
+ $this->pdf->SetFillColor(0, 0, 0);
+ $this->pdf->Polygon(array($x + 5, $y, $x + 11, $y, $x + 8, $y + 5), 'F');
+ $this->pdf->SetFont('dejavusans', '', 8);
+ $this->pdf->Text($x + 30, $y + 3, 'Abgang zum Verbraucher');
+ $y += 15;
+
+ // Norm-Hinweis
+ $y += 10;
+ $this->pdf->SetFont('dejavusans', 'I', 7);
+ $this->pdf->SetTextColor(120, 120, 120);
+ $this->pdf->Text($x, $y, 'Erstellt nach DIN EN 61082 / DIN EN 81346');
+ $this->pdf->Text($x, $y + 5, 'Bezugsbezeichnungen nach DIN EN 81346-2');
+ $this->pdf->Text($x, $y + 10, 'Schaltzeichen nach DIN EN 60617');
+
+ $this->drawTitleBlock(0, 0);
+ }
+
+ /**
+ * Titelfeld nach DIN EN 61082 / ISO 7200
+ */
+ private function drawTitleBlock($pageNum = 0, $totalPages = 0)
+ {
+ $titleBlockWidth = 180;
+ $titleBlockHeight = 56;
+ $titleBlockX = $this->pageWidth - $titleBlockWidth - 10;
+ $titleBlockY = $this->pageHeight - $titleBlockHeight - 10;
+
+ // Rahmen
+ $this->pdf->SetDrawColor(0, 0, 0);
+ $this->pdf->SetLineWidth(0.5);
+ $this->pdf->Rect($titleBlockX, $titleBlockY, $titleBlockWidth, $titleBlockHeight);
+
+ $rowHeight = 8;
+ $col1 = 30; $col2 = 50; $col3 = 50; $col4 = 50;
+
+ // Horizontale Linien
+ for ($i = 1; $i < 7; $i++) {
+ $y = $titleBlockY + ($i * $rowHeight);
+ $this->pdf->Line($titleBlockX, $y, $titleBlockX + $titleBlockWidth, $y);
+ }
+
+ // Vertikale Linien
+ $this->pdf->Line($titleBlockX + $col1, $titleBlockY, $titleBlockX + $col1, $titleBlockY + $titleBlockHeight);
+ $this->pdf->Line($titleBlockX + $col1 + $col2, $titleBlockY, $titleBlockX + $col1 + $col2, $titleBlockY + $titleBlockHeight);
+ $this->pdf->Line($titleBlockX + $col1 + $col2 + $col3, $titleBlockY, $titleBlockX + $col1 + $col2 + $col3, $titleBlockY + $titleBlockHeight);
+
+ $this->pdf->SetTextColor(0, 0, 0);
+
+ // Zeile 1: Titel
+ $this->pdf->SetFont('dejavusans', 'B', 12);
+ $this->pdf->SetXY($titleBlockX + 2, $titleBlockY + 1);
+ $this->pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, 'LEITUNGSLAUFPLAN', 0, 0, 'C');
+
+ // Zeile 2: Anlage
+ $this->pdf->SetFont('dejavusans', 'B', 10);
+ $this->pdf->SetXY($titleBlockX + 2, $titleBlockY + $rowHeight + 1);
+ $this->pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, $this->anlage->label, 0, 0, 'C');
+
+ // Zeile 3: Erstellt | Kunde | Projekt | Blatt
+ $y = $titleBlockY + (2 * $rowHeight);
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Erstellt', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Kunde', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 - 2, 3, 'Projekt-Nr.', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1); $this->pdf->Cell($col4 - 2, 3, 'Blatt', 0, 0);
+
+ $this->pdf->SetFont('dejavusans', '', 8);
+ $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, dol_print_date(dol_now(), 'day'), 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, dol_trunc($this->societe->name, 25), 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 - 2, 4, $this->anlage->ref ?: '-', 0, 0);
+ $blatt = ($pageNum > 0 && $totalPages > 0) ? $pageNum.' / '.$totalPages : '';
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4); $this->pdf->Cell($col4 - 2, 4, $blatt, 0, 0);
+
+ // Zeile 4: Bearbeiter | Adresse | Anlage | Revision
+ $y = $titleBlockY + (3 * $rowHeight);
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Bearbeiter', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Adresse', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 - 2, 3, 'Anlage', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1); $this->pdf->Cell($col4 - 2, 3, 'Revision', 0, 0);
+
+ $this->pdf->SetFont('dejavusans', '', 8);
+ global $langs;
+ $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, dol_trunc($this->user->getFullName($langs), 15), 0, 0);
+ $address = trim(($this->societe->address ?? '').' '.($this->societe->zip ?? '').' '.($this->societe->town ?? ''));
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, dol_trunc($address, 25), 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 - 2, 4, $this->anlage->type_label ?? '-', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4); $this->pdf->Cell($col4 - 2, 4, 'A', 0, 0);
+
+ // Zeile 5: Abgänge | Format | Norm
+ $y = $titleBlockY + (4 * $rowHeight);
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetXY($titleBlockX + 1, $y + 1); $this->pdf->Cell($col1 - 2, 3, 'Abgänge', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 1); $this->pdf->Cell($col2 - 2, 3, 'Format', 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1); $this->pdf->Cell($col3 + $col4 - 2, 3, 'Norm', 0, 0);
+
+ $this->pdf->SetFont('dejavusans', '', 8);
+ $this->pdf->SetXY($titleBlockX + 1, $y + 4); $this->pdf->Cell($col1 - 2, 4, count($this->circuitPaths), 0, 0);
+ $this->pdf->SetXY($titleBlockX + $col1 + 1, $y + 4); $this->pdf->Cell($col2 - 2, 4, $this->format.' '.$this->orientation, 0, 0);
+ $this->pdf->SetFont('dejavusans', '', 6);
+ $this->pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4); $this->pdf->Cell($col3 + $col4 - 2, 4, 'DIN EN 61082 / DIN EN 81346', 0, 0);
+
+ // Zeile 6-7: Firmenname
+ $y = $titleBlockY + (5 * $rowHeight);
+ $this->pdf->SetFont('dejavusans', 'B', 9);
+ $this->pdf->SetXY($titleBlockX + 2, $y + 3);
+ $this->pdf->Cell($titleBlockWidth - 4, $rowHeight * 2 - 6, $GLOBALS['mysoc']->name ?? 'ALLES WATT LÄUFT', 0, 0, 'C');
+ }
+}
diff --git a/tabs/anlagen.php b/tabs/anlagen.php
index cdf005d..d1d8209 100644
--- a/tabs/anlagen.php
+++ b/tabs/anlagen.php
@@ -804,6 +804,11 @@ if (empty($customerSystems)) {
print '';
print ' PDF Export';
print '';
+ // Leitungslaufplan PDF-Export (separates Feature)
+ $wiringUrl = dol_buildpath('/kundenkarte/ajax/export_wiring_diagram_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A3&orientation=L';
+ print '';
+ print ' Leitungslaufplan';
+ print '';
print '';
print '';
print 'Bereit
';
diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php
index fdbabd2..230c9d4 100644
--- a/tabs/contact_anlagen.php
+++ b/tabs/contact_anlagen.php
@@ -802,6 +802,11 @@ if (empty($customerSystems)) {
print '';
print ' PDF Export';
print '';
+ // Leitungslaufplan PDF-Export (separates Feature)
+ $wiringUrl = dol_buildpath('/kundenkarte/ajax/export_wiring_diagram_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A3&orientation=L';
+ print '';
+ print ' Leitungslaufplan';
+ print '';
print '';
print '';
print 'Bereit
';