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 += ' 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
';