Compare commits

..

23 commits
v5.3.0 ... main

Author SHA1 Message Date
785bb9f66f docs: ChangeLog 11.1 ergaenzen
Auto-Benennung Felder/Hutschienen, PWA Abgangs-Labels, SW Bugfix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:26:01 +01:00
b8b2655cd0 fix(sw): chrome-extension URLs aus Cache ausschließen
TypeError beim Cachen von chrome-extension:// Requests verhindert.
Protocol-Check am Anfang des Fetch-Handlers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:07:22 +01:00
71272fa425 fix(schematic): Terminal-Farbpropagierung, Auto-Naming, PWA-Abgänge
- buildTerminalPhaseMap: Schritt 1b - Leitungen mit expliziter Farbe als
  Startpunkte (nur Gerät→Gerät, keine Abgänge)
- buildTerminalPhaseMap: Block-Durchreichung (Top↔Bottom) entfernt
- buildTerminalPhaseMap: Junction-Verbindungen (Terminal→Leitung)
  bidirektional verarbeitet via _connectionById Index
- PWA: Abgangs-Rendering mit Index-Fallback wenn source_terminal_id fehlt
- PWA: Abgangs-Labels max-height 130px, min-height 30px
- Auto-Naming: EquipmentCarrier create/update → 'R' + count
- Auto-Naming: EquipmentPanel update → 'Feld ' + count
- pwa_api.php: Hardcoded Fallbacks 'Feld'/'Hutschiene' entfernt
- pwa.js: Hutschiene Auto-Naming dynamisch aus Panel-Carrier-Anzahl
- kundenkarte.js: Carrier-Dialog Placeholder 'z.B. R1 (automatisch)'
- SW Cache auf v12.5 hochgezählt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:57:58 +01:00
16e51a799a feat(v8.6): Räumlichkeit, Verteilungs-Tabellen, Bundled-Terminals, PWA-Updates
- output_location (Räumlichkeit): Neues Textfeld am Abgang für Raum/Ort des
  Verbrauchers. DB-Migration, Backend (AJAX), Frontend (Website + PWA),
  Anzeige im Schaltplan (kursiv) und in PDF-Tabellen.
- Verteilungs-Tabellen: Kundenansicht (A4, Nr/Verbraucher/Räumlichkeit) und
  Technikeransicht (A4, R.Klem/FI/Nr/Verbraucher/Räumlichkeit/Typ) im
  Leitungslaufplan-PDF. Gruppiert nach Feld/Reihe mit automatischem Seitenumbruch.
- Bundled-Terminals Checkbox: Im Website-Abgang-Dialog (war vorher nur PWA).
- PWA: Diverse Verbesserungen, Service Worker v12.4, Connection-Modal erweitert.
- Typ-Flags: has_product auch für Gebäudetypen, Equipment-Typ Erweiterungen.
- CLAUDE.md + Doku aktualisiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:33:05 +01:00
8826c286ef feat(schematic): Leitungslaufplan PDF-Export nach DIN EN 61082
Neues separates Feature: Stromlaufplan in aufgelöster Darstellung als PDF.

- WiringDiagramAnalyzer: PHP-Port der JS Phase-Map-Logik, Strompfad-Tracing
- WiringDiagramRenderer: TCPDF-Zeichnung mit VDE-Symbolen (LS, FI/RCD)
- PDF enthält: Schaltplan, Abgangsverzeichnis pro Hutschiene, Legende
- Abgangsnummern im Format R{Reihe}.{Position}
- Grüner Button in Schaltplan-Editor (Kunden + Kontakte)
- CLAUDE.md: Dateistruktur komplett überarbeitet, neue Features dokumentiert
- Version 9.7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:06:26 +01:00
4181efacdb feat(schematic): Terminal-Farbpropagierung vom Eingang + Badge-Labels
- Terminal-ColorMap: Farbe wird vom Anschlusspunkt (conn.color) propagiert
  statt aus Standard-Phasenfarben abgeleitet zu werden
- Busbar verteilt nur eingespeiste Phasen mit der Eingangsfarbe (fedColors)
- Block-Durchreichung und Leitungen propagieren Farbe mit
- Eingangs-Bezeichnung als Badge mit Rahmen statt rotiertem Text
- L2-Farbe bleibt bei #1a1a1a (Schwarz) - Änderung auf #555555 rückgängig

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:37:17 +01:00
848232c5a6 feat(schematic): Wire-Dragging, Farbpropagierung, Busbar-Typen
- Wire-Segment-Dragging: Shift+Klick/Mittlere Maustaste zum Verschieben
  - Horizontale Segmente nur vertikal, vertikale nur horizontal
  - Grid-Snapping, Live-Vorschau, Start/End-Segmente fixiert
- PWA: Automatische Farbpropagierung bei Einspeisung (L1/L2/L3/N/PE)
  - N-Phase als Input hinzugefuegt
  - propagateInputColor() aktualisiert Abgaenge
- Busbar-Typen aus Datenbank statt hardcodiert
- ChangeLog aktualisiert

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:46:46 +01:00
95e1860940 Anschlusspunkt Fix 2026-03-04 15:35:25 +01:00
6b3b6d7e95 feat(schematic): Terminal-Farben, Leitungen hinter Blöcken, Zeichenmodus v11.0
Terminal-Farben nach Verbindung:
- Terminals zeigen Farbe der angeschlossenen Leitung
- Grau = keine Verbindung, farbig = Leitung angeschlossen
- Neue Hilfsfunktion getTerminalConnectionColor()

Leitungen hinter Blöcken:
- Layer-Reihenfolge geändert: connections vor blocks
- Professionelleres Erscheinungsbild

Zeichenmodus-Verbesserungen:
- Rechtsklick/Escape bricht nur Linie ab, nicht Modus
- Crosshair-Cursor überall im SVG während Zeichenmodus
- 30px Hit-Area für bessere Klickbarkeit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 13:44:52 +01:00
4540b8c595 feat(schematic): Junction-Verbindungen Terminal zu Leitung
- Terminal zu Leitung Verbindungen (Junction) implementiert
- Snap zum nächsten Punkt auf der Leitung
- Cleanup von überflüssigen Pfad-Punkten (cleanupPathPoints)
- Terminal-Position nutzt jetzt col-Property aus terminals_config
- Einheitlicher Dialog für alle Verbindungstypen
- Kleinere Hit-Area für Terminals (verhindert Überlappen)
- SQL für anlage_accessory Tabelle hinzugefügt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 12:57:25 +01:00
be3a53e77e fix(schematic): Orthogonale Pfade erzwingen
Beim Zeichnen werden jetzt automatisch Zwischenpunkte eingefügt,
um sicherzustellen dass alle Liniensegmente horizontal oder
vertikal sind (90°-Winkel, keine Diagonalen).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 12:24:36 +01:00
89a4db4d21 fix(schematic): Grid-Punkte exakt auf Terminal-Positionen
Das Wire-Grid wird jetzt direkt aus den Equipment-Terminal-Positionen
berechnet, identisch zu getTerminalPosition():

- X: eq._x + teIndex * TE_WIDTH + TE_WIDTH/2
- Y: eq._y - 5 (top) / eq._y + height + 5 (bottom)

Vorher wurden Grid-Punkte aus TE-Slots berechnet, was nicht den
+2px Equipment-Offset berücksichtigte.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 08:41:03 +01:00
50ae4e4a08 fix(pwa): Terminal-Ausrichtung, gebündelte Pfeile, Block-Value Größe
- Gebündelte Terminals: Pfeil jetzt in Zeile 2/4 statt Label-Zeile
- Terminal-Punkte mit CSS-Klassen terminal-row-top/bottom
- Equipment-Block-Value auf 8px verkleinert
- Grid gap auf 0 für kompaktere Darstellung
- Service Worker v6.1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 08:34:17 +01:00
5f23727202 fix(schematic): Manuelles Zeichnen - Koordinaten und Raster korrigiert
Problem: Linien verschoben sich während des Zeichnens und waren nicht
korrekt an Start-/Endpunkten ausgerichtet. Raster fehlte außerhalb
der Hutschienen.

Ursache:
- setupMagneticSnap() verwendete getBoundingClientRect() (DOM-Pixel)
  statt SVG-Koordinaten über createSVGPoint().matrixTransform()
- Raster wurde nur aus Hutschienen-TE berechnet, keine Randlinien

Lösung:
- Korrekte SVG-Koordinaten in setupMagneticSnap() via matrixTransform
- Erweitertes Raster: Links/rechts Rand (alle 20px), mehr Y-Linien
  ober- und unterhalb der Blöcke (alle 15px statt 20px)
- Snap-Radius von 20 auf 25 SVG-Einheiten erhöht

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 08:34:01 +01:00
7de0349808 fix(select): Leerzeichen in Pipe-getrennten Select-Optionen trimmen
field_options wie "Ferris|Digital 1 Richtung| Digital 2 Richtung" führten
zu Leerzeichen nach split('|'), wodurch der Vergleich mit dem gespeicherten
Wert fehlschlug und das Select-Feld beim Editieren leer blieb.

Fix: opt.trim() in JS bei allen Select-Renderings (DynamicFields,
SchematicEditor, PWA) + automatisches Trimmen beim Speichern in Admin.
DB-Werte in Produktion bereinigt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:34:00 +01:00
65f24495e6 docs: Changelog v8.5/v8.6 und CLAUDE.md aktualisiert
- ChangeLog: v8.5 (Werkzeuge, Zubehör, Ausgebaut) und v8.6 (has_product, Decommissioned-Default, Select2-Fix)
- CLAUDE.md: Neue Abschnitte für Ausgebaut-Status, Mein Betrieb, Select2-Kategorie-Filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:23:48 +01:00
c4338c8d7a feat: has_product Typ-Flag, Decommissioned-Default, Select2-Bugfix
- has_product Flag für Element- und Gebäudetypen im Admin-Center
- Migration v8.6.0: has_product Spalte in beide Typ-Tabellen
- Produkt-Zeile im Formular wird per JS je nach Typ ein-/ausgeblendet
- Admin-Setting KUNDENKARTE_SHOW_DECOMMISSIONED für Standard-Sichtbarkeit
- Toggle-Button + Tree-Klasse nutzen Admin-Default in allen 3 Ansichten
- Fix: Select2 Typ-Select behält Wert im Edit-Modus (trigger nach init)
- Fix: Fehlende color-Property in fetchAllBySystem() ergänzt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:21:38 +01:00
3b9daeb238 feat: System-Tabs für Mein Betrieb, Produkt/Zubehör für Kunden+Kontakte
- werkzeuge.php: Multi-System mit System-Tabs (nicht mehr nur WERKZEUG)
  - Systeme hinzufügen/entfernen wie bei Kunden-Anlagen
  - Alle URLs mit system-Parameter versehen
  - Steuerungs-Buttons in System-Tab-Wrapper integriert

- tabs/anlagen.php + tabs/contact_anlagen.php:
  - Produkt-Zuordnung im Create/Edit-Formular (Autocomplete)
  - Produkt-Anzeige in der Detailansicht
  - Zubehör-Liste mit Hinzufügen/Löschen
  - Lieferantenbestellung aus Zubehör
  - fk_product in add/update-Actions aufgenommen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:41:30 +01:00
7a548c87e2 fix: Mein Betrieb - virtuelle socId und generischerer Name
- Virtuelle ID 99999999 statt $mysoc->id (ist immer 0 in Dolibarr)
- Umbenennung "Firmen-Werkzeuge" → "Mein Betrieb" für mehrere Systeme
- Unnötige Societe/Company-Includes entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:29:06 +01:00
a14b33b7c7 feat: Firmen-Werkzeuge, Zubehör-System und Produkt-Zuordnung
- Neue Seite werkzeuge.php mit Baumansicht für Firmen-Maschinen/Werkzeuge
- Menüpunkt "Firmen-Werkzeuge" unter Start-Menü
- Neue Klasse AnlageAccessory für Zubehör/Ersatzteile pro Anlage
- AJAX-Endpunkt ajax/anlage_accessory.php (CRUD + Lieferantenbestellung)
- DB: fk_product auf Anlage, has_accessories auf AnlageType, Zubehör-Tabelle
- Neues System WERKZEUG in Systemkategorien
- Admin: Checkbox "Hat Zubehör" im Typ-Editor
- Produkt-Autocomplete, Zubehör-Liste mit Bestellfunktion (CommandeFournisseur)
- Produkt-JOIN in fetchChildren für product_ref im Baum
- Übersetzungen de_DE + en_US

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:23:29 +01:00
66abaa088d feat: Ausbau-Datum in Detailansicht anzeigen
Zeigt den Ausgebaut-Status mit Datum in der Element-Detailansicht
(action=view) für beide Tab-Dateien.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:06:31 +01:00
aa67a77d16 fix: AJAX-URL-Pfade für Ausbauen-Funktion korrigiert
KundenKarte.ajaxUrl/token existiert nicht - ersetze durch
baseUrl + '/custom/kundenkarte/ajax/...' (wie alle anderen
AJAX-Aufrufe im Modul).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:03:34 +01:00
fd8d11e764 feat: Ausgebaut-Status für Anlagen-Elemente
Elemente können als "ausgebaut" markiert werden mit Datum.
Bleiben in der DB für Nachvollziehbarkeit, werden ausgegraut
dargestellt und können per Toggle ein-/ausgeblendet werden.

- DB: decommissioned + date_decommissioned Spalten
- Dialog mit Datumsauswahl beim Ausbauen
- Toggle-Button in Baum- und Graph-Toolbar
- Ausgebaute Elemente ausgegraut (opacity 0.4, durchgestrichen)
- Badge mit Ausbau-Datum im Baum
- Kontextmenü im Graph für Ausbauen/Einbauen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:49:31 +01:00
45 changed files with 10355 additions and 507 deletions

209
CLAUDE.md
View file

@ -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 + Verteilungs-Tabellen (~2.130 Zeilen)
### 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 (~15.600 Zeilen)
- `js/kundenkarte_cytoscape.js` - Graph-JS (~900 Zeilen)
- `js/pwa.js` - PWA-JS (~3.400 Zeilen)
- `css/kundenkarte.css` - Alle Styles (Dark Mode Theme)
- `css/pwa.css` - PWA-Styles
## Wichtige Hinweise
@ -135,7 +183,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
- `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter)
- `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen
- `sw.js` - Service Worker für Offline-Cache (v6.1)
- `sw.js` - Service Worker für Offline-Cache (v12.4)
- `manifest.json` - Web App Manifest für Installation
### Workflow
@ -212,7 +260,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
- `bundled_terminals = 'all'` in Connection bedeutet: Alle Terminals belegt
- Im Editor: Ein Pfeil spannt über alle Terminals des Equipment
- Label wird zentriert über alle Terminals angezeigt
- Checkbox "Alle bündeln" nur bei Equipment mit >1 Terminal sichtbar
- Checkbox "Alle Terminals bündeln" im Abgang-Dialog (Website + PWA), nur bei Equipment mit >1 Terminal
### Terminal-Konfiguration (v7.5)
- `terminals_config` JSON im Equipment-Typ definiert Terminal-Positionen
@ -226,3 +274,154 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
- Zeile 3: Equipment-Blöcke
- Zeile 4: Terminal-Punkte unten (terminal-point.terminal-row-bottom)
- Zeile 5: Abgang-Labels unten (terminal-label-cell.label-row-bottom)
## Ausgebaut-Status (v8.0)
### Spalten
- `decommissioned` (tinyint DEFAULT 0) in `llx_kundenkarte_anlage`
- `date_decommissioned` (date NULL) in `llx_kundenkarte_anlage`
### Verhalten
- Toggle per Button am Element im Baum und Graph
- Ausgebaute Elemente: `opacity: 0.4`, dashed border, Badge "Ausgebaut"
- Toggle-Button in Toolbar: Klasse `.show-decommissioned` auf `.kundenkarte-tree`
- Admin-Setting `KUNDENKARTE_SHOW_DECOMMISSIONED` für Standard-Sichtbarkeit
- Graph-View: Nodes mit Klasse `.decommissioned` (35% opacity, dashed border)
## Mein Betrieb / Werkzeuge (v8.5)
### Übersicht
Eigene Seite für Firmen-Equipment (Werkzeuge, Maschinen, Messgeräte).
### Dateien
- `werkzeuge.php` - Baumansicht für eigene Firma (fk_soc = mysoc->id)
- `class/anlageaccessory.class.php` - Zubehör-Klasse mit CRUD + Bestellfunktion
- `ajax/anlage_accessory.php` - AJAX-Endpunkte für Zubehör
### System
- Neues System `WERKZEUG` (ID 26) in `llx_c_kundenkarte_anlage_system`
- Menüpunkt unter KundenKarte > Mein Betrieb
- System-Filter fix auf "WERKZEUG"
### Produkt-Zuordnung
- `fk_product` in `llx_kundenkarte_anlage` verknüpft mit Dolibarr-Produkt
- Autocomplete-Suche via `ajax/equipment.php?action=get_products`
- Anzeige: Ref + Label + Preis unter Element im Baum
- **Typ-Flag `has_product`**: Steuert ob Produkt-Zeile im Formular sichtbar ist
- `data-has-product` Attribut auf `<option>` für JS-Steuerung
### Zubehör-System
- Tabelle `llx_kundenkarte_anlage_accessory` (fk_anlage, fk_product, qty, rang, note)
- 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 (~2.130 Zeilen)
- `WiringDiagramAnalyzer` — Lädt Daten, baut Phase-Map (PHP-Port), tracet Strompfade
- `WiringDiagramRenderer` — Zeichnet PDF mit TCPDF
- `ajax/export_wiring_diagram_pdf.php` — Endpoint
- Buttons in `tabs/anlagen.php` + `tabs/contact_anlagen.php` (je 4 Zeilen)
### PDF-Inhalt (5 Teile)
1. **Leitungslaufplan** (A3 quer) — L1/L2/L3 horizontal oben, vertikale Strompfade pro Abgang, FI/RCD + LS-Symbole, Abgang-Pfeile, N/PE unten
2. **Abgangsverzeichnis** (A3 quer) — Tabelle pro Hutschiene mit: Abg.Nr, Bezeichnung, Phase, Absicherung, Kabel, Schutzgerät
3. **Kundenansicht** (A4 hoch) — `renderKundenansicht()` — Einfache Tabelle: Nr | Verbraucher | Räumlichkeit, gruppiert nach Feld/Reihe
4. **Technikeransicht** (A4 hoch) — `renderTechnikeransicht()` — Erweiterte Tabelle: R.Klem | FI | Nr | Verbraucher | Räumlichkeit | Typ
5. **Mini-Legende** — Phasenfarben DIN VDE auf Seite 1 unten links
### Abgangsnummer-Format
`R{Reihe}.{Position}` z.B. `R1.3` = Carrier-Position 1, Equipment-TE-Position 3
### 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)
## Räumlichkeit / output_location (v8.6)
### Übersicht
Zusätzliches Textfeld am Abgang (Output-Connection) für den Raum/Ort des Verbrauchers (z.B. "Küche", "Bad OG").
### Datenbank
- Spalte `output_location` (varchar 255) in `llx_kundenkarte_equipment_connection`
- Migration: `migrate_v1110_output_location()` in `modKundenKarte.class.php`
### Backend
- `EquipmentConnection::$output_location` — Property, in create/update/fetch
- `ajax/equipment_connection.php` — create_output + update + list_all
- `ajax/pwa_api.php` — get_carrier_equipment + create_connection + update_connection
### Frontend
- **Website**: Eingabefeld im `renderAbgangDialog()` (kundenkarte.js)
- **PWA**: Eingabefeld `#conn-location` im Connection-Modal (pwa.php + pwa.js)
- **Anzeige im Schaltplan**:
- Website SVG: `<tspan>` kursiv nach Label mit ` · ` Trennzeichen
- PWA Grid: `<span class="output-location">` kursiv unter dem Label-Text
- **PDF**: In Kundenansicht + Technikeransicht als eigene Tabellenspalte
## Select2 mit Kategorie-Filter
### Problem & Lösung
In anlagen.php und contact_anlagen.php gibt es einen Kategorie-Filter (Gebäude/Element),
der die Typ-Options per JS filtert und Select2 neu initialisiert.
**Wichtig**: Nach `initSelect2()` muss der Wert mit `.trigger("change")` gesetzt werden,
damit Select2 den aktuellen Wert korrekt anzeigt:
```javascript
initSelect2();
if (currentVal && $typeSelect.find('option[value="' + currentVal + '"]').length) {
$typeSelect.val(currentVal).trigger("change");
}
```
### Ablauf in filterTypes()
1. `currentVal` sichern
2. HTML aus `allOptionsHtml` zurücksetzen
3. Nicht passende Options entfernen
4. Select2 initialisieren
5. Wert mit `.trigger("change")` wiederherstellen

View file

@ -1,5 +1,158 @@
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 11.1 (2026-03)
### Neue Features
- **Auto-Benennung Felder und Hutschienen**: Automatischer Name wenn Label leer gelassen wird
- Felder: "Feld N" (N = Anzahl Felder in der Anlage)
- Hutschienen: "RN" (N = Anzahl Hutschienen im Panel)
- Gilt sowohl beim Erstellen als auch beim Bearbeiten (leer lassen = Auto-Name)
- Funktioniert in PWA und Website gleichermassen
- PHP-Klassen `EquipmentPanel` und `EquipmentCarrier` berechnen Namen server-seitig
- PWA berechnet Namen client-seitig (inkl. korrektem +1 bei CREATE vs. ohne bei UPDATE)
- Website: Carrier-Dialog zeigt Platzhalter "z.B. R1 (automatisch)"
### Verbesserungen
- **PWA: Mehr Platz fuer Abgangs-Labels**: Lange Leitungsbezeichnungen vollstaendig sichtbar
- `.terminal-label` max-height: 80px → 130px
- `.terminal-label-cell` min-height: 20px → 30px
### Bugfixes
- **Service Worker**: chrome-extension:// URLs verursachten TypeError beim Cachen
- Protocol-Check am Anfang des Fetch-Handlers verhindert Fehler
- Betrifft Browser mit installierten Extensions (Chrome, Edge)
---
## 11.0 (2026-03)
### Neue Features
- **Terminal-Farben nach Verbindung**: Terminals zeigen die Farbe der angeschlossenen Leitung
- Grau = keine Verbindung, farbig = Leitung angeschlossen
- Farbe entspricht dem Leitungstyp (L1=braun, L2=schwarz, L3=grau, N=blau, PE=gruen)
- Neue Hilfsfunktion `getTerminalConnectionColor()`
- **Leitungen hinter Bloecken**: Wires werden nun hinter den Equipment-Bloecken gerendert
- Layer-Reihenfolge geaendert: connections-layer vor blocks-layer
- Leitungen "verschwinden" hinter Bloecken und kommen auf der anderen Seite wieder raus
- Professionelleres Erscheinungsbild wie in echten Schaltplan-Editoren
- **Wire-Segment-Dragging**: Leitungen koennen verschoben werden ohne Verbindungen zu verlieren
- Shift+Klick oder Mittlere Maustaste auf Leitungssegment zum Ziehen
- Horizontale Segmente nur vertikal verschiebbar, vertikale nur horizontal
- Start- und End-Segmente (an Terminals) bleiben fix
- Automatisches Grid-Snapping (25px)
- Live-Vorschau waehrend dem Ziehen
- Neue Funktionen: `parsePathToPoints()`, `pointsToPath()`, `findClickedSegment()`
- `startWireDrag()`, `handleWireDragMove()`, `finishWireDrag()`, `cancelWireDrag()`
- **Busbar-Typen aus Datenbank**: Phasenschienen-Typen dynamisch aus DB laden
- Edit-Dialog nutzt jetzt `busbarTypes` Array statt hardcodierter Optionen
- `fk_busbar_type` wird beim Update korrekt gespeichert
- Admin-Seite fuer Busbar-Typen mit phases_config JSON-Feld
- **PWA: Farbpropagierung bei Einspeisung**: Automatische Farbuebernahme
- Bei Auswahl einer Input-Phase (L1/L2/L3/N/PE) wird Farbe automatisch gesetzt
- Funktion `propagateInputColor()` aktualisiert Farben auf Abgaengen
- Phase-Matching: L1 matched L1, LN, L1N; N matched N; etc.
- Funktioniert online und offline (mit Queue)
- **PWA: N-Phase als Einspeisung**: Neutralleiter jetzt als Input-Phase waehlbar
- INPUT_PHASES erweitert um 'N': ['L1', 'L2', 'L3', 'N', 'PE']
- 3P und 3P+N entfernt (nur Einzel-Phasen)
### Verbesserungen
- **Zeichenmodus-Verhalten**: Konsistentes Verhalten im manuellen Zeichenmodus
- Rechtsklick bricht nur aktuelle Linie ab, nicht den Zeichenmodus
- Escape-Taste verhaelt sich identisch (Linie abbrechen, Modus bleibt aktiv)
- Crosshair-Cursor ueberall im SVG waehrend Zeichenmodus (nicht nur auf Terminals)
- Keine Unterbrechung mehr beim Klicken auf leere Flaechen
- **Terminal Hit-Area**: Verbesserte Klickbarkeit der Terminals
- 30px Radius fuer einfacheres Anklicken
- `pointer-events:all` fuer zuverlaessige Event-Erfassung
### Bugfixes
- Cursor zeigte nicht ueberall Crosshair im Zeichenmodus
- Zeichenmodus wurde bei Rechtsklick/Escape komplett beendet statt nur Linie abzubrechen
- Junction-Verbindungen (Terminal zu Leitung) werden korrekt gerendert
---
## 8.6 (2026-03)
### Neue Features
- **has_product Typ-Flag**: Produkt-Zuordnung pro Element- und Gebaeudetyp ein-/abschaltbar
- Neue Checkbox "Produkt-Zuordnung" in Admin > Element-Typen und Gebaeudetypen
- Produkt-Zeile im Formular wird per JS dynamisch ein-/ausgeblendet je nach Typ
- `data-has-product` und `data-has-accessories` Attribute auf Options fuer JS-Steuerung
- **Decommissioned Default-Setting**: Standard-Sichtbarkeit fuer ausgebaute Elemente
- Neues Admin-Setting `KUNDENKARTE_SHOW_DECOMMISSIONED` unter Einstellungen
- Toggle-Button startet mit Admin-Default in allen 3 Ansichten (Kunden, Kontakte, Mein Betrieb)
- Tree-Div erhaelt `show-decommissioned` CSS-Klasse basierend auf Setting
### Bugfixes
- **Select2 Typ-Select im Edit-Modus**: Wert wurde nicht angezeigt beim Bearbeiten
- Ursache: Wert wurde vor Select2-Initialisierung gesetzt ohne `.trigger("change")`
- Fix: Wert wird jetzt nach `initSelect2()` mit Trigger gesetzt
- Betrifft: anlagen.php und contact_anlagen.php (Kategorie-Filter mit Select2)
- **Fehlende color-Property**: `fetchAllBySystem()` hat `$type->color` nicht gesetzt
- Options mit `data-color` Attribut hatten leeren Wert
### Datenbank-Aenderungen
- Neue Spalte `has_product` (tinyint) in `llx_kundenkarte_anlage_type`
- Neue Spalte `has_product` (tinyint) in `llx_kundenkarte_building_type`
- Migration `migrate_v860_has_product()` in modKundenKarte.class.php
---
## 8.5 (2026-03)
### Neue Features
- **Mein Betrieb (Werkzeuge & Maschinen)**: Eigene Baumansicht fuer Firmen-Equipment
- Neue Seite `werkzeuge.php` mit System-Tabs, Baum und Graph-Ansicht
- Neues System "WERKZEUG" fuer firmeneigene Geraete
- Menue-Eintrag unter KundenKarte > Mein Betrieb
- **Zubehoer-System**: Zubehoer und Ersatzteile pro Anlage zuordnen
- Neue Klasse `AnlageAccessory` mit CRUD und Bestellfunktion
- Lieferantenbestellung direkt aus Zubehoer-Liste generierbar
- Typ-Flag `has_accessories` steuert Verfuegbarkeit pro Typ
- **Produkt-Zuordnung**: Dolibarr-Produkt mit Anlage verknuepfen
- `fk_product` Spalte in `llx_kundenkarte_anlage`
- Autocomplete-Suche im Formular
- Produkt-Details (Ref, Label, Preis) unter Element im Baum
- **Ausgebaut-Status**: Anlagen als "ausgebaut" markieren
- Toggle per Rechtsklick/Button am Element
- Ausgebaute Elemente ausgegraut (opacity 0.4, dashed border)
- Toggle-Button in Toolbar zum Ein-/Ausblenden
- Ausbaudatum wird erfasst und angezeigt
### Datenbank-Aenderungen
- Neue Spalte `decommissioned` in `llx_kundenkarte_anlage`
- Neue Spalte `date_decommissioned` in `llx_kundenkarte_anlage`
- Neue Spalte `fk_product` in `llx_kundenkarte_anlage`
- Neue Spalte `has_accessories` in `llx_kundenkarte_anlage_type`
- Neue Tabelle `llx_kundenkarte_anlage_accessory`
- Neues System `WERKZEUG` in `llx_c_kundenkarte_anlage_system`
---
## 7.5 (2026-03)
### Neue Features

View file

@ -63,6 +63,8 @@ if ($action == 'add') {
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
$anlageType->has_accessories = GETPOSTINT('has_accessories');
$anlageType->has_product = GETPOSTINT('has_product');
$anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position');
@ -113,6 +115,8 @@ if ($action == 'update') {
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
$anlageType->has_accessories = GETPOSTINT('has_accessories');
$anlageType->has_product = GETPOSTINT('has_product');
$anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position');
@ -166,6 +170,8 @@ if ($action == 'copy' && $typeId > 0) {
$newType->can_be_nested = $sourceType->can_be_nested;
$newType->allowed_parent_types = $sourceType->allowed_parent_types;
$newType->can_have_equipment = $sourceType->can_have_equipment;
$newType->has_accessories = $sourceType->has_accessories;
$newType->has_product = $sourceType->has_product;
$newType->picto = $sourceType->picto;
$newType->color = $sourceType->color;
$newType->position = $sourceType->position + 1;
@ -202,6 +208,10 @@ if ($action == 'add_field') {
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
$fieldOptions = GETPOST('field_options', 'nohtml');
// Leerzeichen um Pipe-Trennzeichen entfernen und leere Optionen entfernen
if ($fieldOptions) {
$fieldOptions = implode('|', array_filter(array_map('trim', explode('|', $fieldOptions)), 'strlen'));
}
$showInTree = GETPOSTINT('show_in_tree');
$treeDisplayMode = GETPOST('tree_display_mode', 'aZ09');
if (empty($treeDisplayMode)) $treeDisplayMode = 'badge';
@ -237,6 +247,10 @@ if ($action == 'update_field') {
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
$fieldOptions = GETPOST('field_options', 'nohtml');
// Leerzeichen um Pipe-Trennzeichen entfernen und leere Optionen entfernen
if ($fieldOptions) {
$fieldOptions = implode('|', array_filter(array_map('trim', explode('|', $fieldOptions)), 'strlen'));
}
$showInTree = GETPOSTINT('show_in_tree');
$treeDisplayMode = GETPOST('tree_display_mode', 'aZ09');
if (empty($treeDisplayMode)) $treeDisplayMode = 'badge';
@ -402,6 +416,16 @@ if (in_array($action, array('create', 'edit'))) {
print '<td><input type="checkbox" name="can_have_equipment" value="1"'.($anlageType->can_have_equipment ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('CanHaveEquipmentHelp').')</span></td></tr>';
// Hat Zubehör (Zubehör/Ersatzteile zuordnen)
print '<tr><td>'.$langs->trans('HasAccessories').'</td>';
print '<td><input type="checkbox" name="has_accessories" value="1"'.($anlageType->has_accessories ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('HasAccessoriesHelp').')</span></td></tr>';
// Produkt-Zuordnung erlauben
print '<tr><td>'.$langs->trans('HasProduct').'</td>';
print '<td><input type="checkbox" name="has_product" value="1"'.($anlageType->has_product ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('HasProductHelp').')</span></td></tr>';
// Allowed parent types - with multi-select UI
print '<tr><td>'.$langs->trans('AllowedParentTypes').'</td>';
print '<td>';

View file

@ -42,6 +42,7 @@ if ($action == 'add' && $user->admin) {
$buildingType->icon = GETPOST('icon', 'alphanohtml');
$buildingType->color = GETPOST('color', 'alphanohtml');
$buildingType->can_have_children = GETPOSTINT('can_have_children');
$buildingType->has_product = GETPOSTINT('has_product');
$buildingType->position = GETPOSTINT('position');
$buildingType->active = GETPOSTINT('active');
@ -82,6 +83,7 @@ if ($action == 'update' && $user->admin) {
$buildingType->icon = GETPOST('icon', 'alphanohtml');
$buildingType->color = GETPOST('color', 'alphanohtml');
$buildingType->can_have_children = GETPOSTINT('can_have_children');
$buildingType->has_product = GETPOSTINT('has_product');
$buildingType->position = GETPOSTINT('position');
$buildingType->active = GETPOSTINT('active');
@ -222,6 +224,12 @@ if ($action == 'create' || $action == 'edit') {
print '<input type="checkbox" name="can_have_children" value="1"'.($buildingType->can_have_children || $action != 'edit' ? ' checked' : '').'>';
print '</td></tr>';
// Produkt-Zuordnung erlauben
print '<tr><td>'.$langs->trans('HasProduct').'</td><td>';
print '<input type="checkbox" name="has_product" value="1"'.($buildingType->has_product ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('HasProductHelp').')</span>';
print '</td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td><td>';
$defaultPos = $action == 'create' ? $buildingType->getNextPosition() : $buildingType->position;

View file

@ -85,6 +85,12 @@ if ($action == 'add') {
$busbarType->description = GETPOST('description', 'restricthtml');
$busbarType->fk_system = GETPOSTINT('fk_system');
$busbarType->phases = GETPOST('phases', 'alphanohtml');
// Convert comma-separated phases_config to JSON array
$phasesConfigInput = GETPOST('phases_config', 'alphanohtml');
if (!empty($phasesConfigInput)) {
$arr = array_map('trim', explode(',', $phasesConfigInput));
$busbarType->phases_config = json_encode($arr);
}
$busbarType->num_lines = GETPOSTINT('num_lines');
$busbarType->color = GETPOST('color', 'alphanohtml');
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
@ -120,6 +126,14 @@ if ($action == 'update') {
$busbarType->description = GETPOST('description', 'restricthtml');
$busbarType->fk_system = GETPOSTINT('fk_system');
$busbarType->phases = GETPOST('phases', 'alphanohtml');
// Convert comma-separated phases_config to JSON array
$phasesConfigInput = GETPOST('phases_config', 'alphanohtml');
if (!empty($phasesConfigInput)) {
$arr = array_map('trim', explode(',', $phasesConfigInput));
$busbarType->phases_config = json_encode($arr);
} else {
$busbarType->phases_config = null;
}
$busbarType->num_lines = GETPOSTINT('num_lines');
$busbarType->color = GETPOST('color', 'alphanohtml');
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
@ -267,6 +281,20 @@ if ($action == 'create' || $action == 'edit') {
print '<tr><td>'.$langs->trans('NumLines').'</td>';
print '<td><input type="number" name="num_lines" id="numlines-input" class="flat" value="'.($busbarType->num_lines ?: 1).'" min="1" max="10"></td></tr>';
// Phase labels per line (phases_config)
$phasesConfigVal = '';
if (!empty($busbarType->phases_config)) {
$arr = json_decode($busbarType->phases_config, true);
if (is_array($arr)) {
$phasesConfigVal = implode(',', $arr);
}
}
print '<tr><td>'.$langs->trans('PhaseLabels').'</td>';
print '<td>';
print '<input type="text" name="phases_config" id="phases-config-input" class="flat minwidth200" value="'.dol_escape_htmltag($phasesConfigVal).'" placeholder="L1,L2,L3">';
print '<div class="opacitymedium small">Kommagetrennte Bezeichnungen pro Linie, wiederholen sich (z.B. L1,L2,L3 oder L1,N)</div>';
print '</td></tr>';
// Colors
print '<tr><td>'.$langs->trans('Colors').'</td>';
print '<td>';

View file

@ -228,6 +228,10 @@ if ($action == 'add_field') {
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
$fieldOptions = GETPOST('field_options', 'nohtml');
// Leerzeichen um Pipe-Trennzeichen entfernen und leere Optionen entfernen
if ($fieldOptions) {
$fieldOptions = implode('|', array_filter(array_map('trim', explode('|', $fieldOptions)), 'strlen'));
}
$showInHover = GETPOSTINT('show_in_hover');
$showOnBlock = GETPOSTINT('show_on_block');
$isRequired = GETPOSTINT('is_required');
@ -256,6 +260,10 @@ if ($action == 'update_field') {
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
$fieldOptions = GETPOST('field_options', 'nohtml');
// Leerzeichen um Pipe-Trennzeichen entfernen und leere Optionen entfernen
if ($fieldOptions) {
$fieldOptions = implode('|', array_filter(array_map('trim', explode('|', $fieldOptions)), 'strlen'));
}
$showInHover = GETPOSTINT('show_in_hover');
$showOnBlock = GETPOSTINT('show_on_block');
$isRequired = GETPOSTINT('is_required');

View file

@ -125,6 +125,9 @@ if ($action == 'update') {
// View mode
dolibarr_set_const($db, 'KUNDENKARTE_DEFAULT_VIEW', GETPOST('KUNDENKARTE_DEFAULT_VIEW', 'aZ09'), 'chaine', 0, '', $conf->entity);
// Ausgebaute Elemente standardmäßig anzeigen
dolibarr_set_const($db, 'KUNDENKARTE_SHOW_DECOMMISSIONED', GETPOSTINT('KUNDENKARTE_SHOW_DECOMMISSIONED'), 'chaine', 0, '', $conf->entity);
// Tree display settings
dolibarr_set_const($db, 'KUNDENKARTE_TREE_INFO_DISPLAY', GETPOST('KUNDENKARTE_TREE_INFO_DISPLAY', 'aZ09'), 'chaine', 0, '', $conf->entity);
dolibarr_set_const($db, 'KUNDENKARTE_TREE_BADGE_COLOR', GETPOST('KUNDENKARTE_TREE_BADGE_COLOR', 'alphanohtml'), 'chaine', 0, '', $conf->entity);
@ -207,6 +210,14 @@ print $form->selectarray('KUNDENKARTE_DEFAULT_VIEW', $viewModes, getDolGlobalStr
print '</td>';
print '</tr>';
// Ausgebaute Elemente standardmäßig anzeigen
print '<tr class="oddeven">';
print '<td>'.$langs->trans("ShowDecommissionedDefault").'</td>';
print '<td>';
print $form->selectyesno('KUNDENKARTE_SHOW_DECOMMISSIONED', getDolGlobalInt('KUNDENKARTE_SHOW_DECOMMISSIONED', 0), 1);
print '</td>';
print '</tr>';
print '</table>';
// Tree Display Settings

View file

@ -48,7 +48,8 @@ function treeToArray($nodes) {
'fk_parent' => $node->fk_parent,
'fk_system' => $node->fk_system,
'type_label' => $node->type_label,
'status' => $node->status
'status' => $node->status,
'decommissioned' => (int) $node->decommissioned
);
if (!empty($node->children)) {
$item['children'] = treeToArray($node->children);
@ -94,7 +95,8 @@ switch ($action) {
'display_label' => $prefix . $node->label,
'fk_parent' => $node->fk_parent,
'type_label' => $node->type_label,
'status' => $node->status
'status' => $node->status,
'decommissioned' => (int) $node->decommissioned
);
if (!empty($node->children)) {
$flattenTree($node->children, $prefix . ' ');
@ -123,6 +125,7 @@ switch ($action) {
'type_label' => $anlage->type_label,
'fk_system' => $anlage->fk_system,
'status' => $anlage->status,
'decommissioned' => (int) $anlage->decommissioned,
'field_values' => $anlage->getFieldValues()
);
} else {
@ -130,6 +133,36 @@ switch ($action) {
}
break;
case 'toggle_decommissioned':
// Ausgebaut-Status umschalten
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
if ($anlageId > 0 && $anlage->fetch($anlageId) > 0) {
$anlage->decommissioned = $anlage->decommissioned ? 0 : 1;
if ($anlage->decommissioned) {
// Ausbau: Datum setzen (aus POST oder heute)
$dateStr = GETPOST('date_decommissioned', 'alpha');
$anlage->date_decommissioned = !empty($dateStr) ? $dateStr : date('Y-m-d');
} else {
// Wieder einbauen: Datum löschen
$anlage->date_decommissioned = null;
}
$result = $anlage->update($user);
if ($result > 0) {
$response['success'] = true;
$response['decommissioned'] = (int) $anlage->decommissioned;
$response['date_decommissioned'] = $anlage->date_decommissioned;
} else {
$response['error'] = 'Fehler beim Speichern';
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'reorder':
// Reihenfolge der Elemente aktualisieren
if (!$user->hasRight('kundenkarte', 'write')) {

154
ajax/anlage_accessory.php Executable file
View file

@ -0,0 +1,154 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX-Endpunkt für Anlagen-Zubehör Operationen
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$response = array('success' => false, 'error' => '');
// Berechtigungsprüfung
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$accessory = new AnlageAccessory($db);
switch ($action) {
case 'list':
// Alle Zubehörteile einer Anlage laden
$anlageId = GETPOSTINT('fk_anlage');
if ($anlageId > 0) {
$accessories = $accessory->fetchAllByAnlage($anlageId);
$result = array();
foreach ($accessories as $acc) {
$result[] = array(
'id' => $acc->id,
'fk_product' => $acc->fk_product,
'product_ref' => $acc->product_ref,
'product_label' => $acc->product_label,
'product_price' => $acc->product_price,
'qty' => $acc->qty,
'note' => $acc->note,
);
}
$response['success'] = true;
$response['accessories'] = $result;
} else {
$response['error'] = 'Missing fk_anlage';
}
break;
case 'add':
// Zubehör hinzufügen
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$accessory->fk_anlage = GETPOSTINT('fk_anlage');
$accessory->fk_product = GETPOSTINT('fk_product');
$accessory->qty = GETPOSTINT('qty') > 0 ? GETPOSTINT('qty') : 1;
$accessory->note = GETPOST('note', 'alphanohtml');
$result = $accessory->create($user);
if ($result > 0) {
$response['success'] = true;
$response['id'] = $result;
} else {
$response['error'] = $accessory->error ?: 'Fehler beim Speichern';
}
break;
case 'update':
// Zubehör aktualisieren (Menge, Notiz)
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$id = GETPOSTINT('id');
if ($id > 0 && $accessory->fetch($id) > 0) {
$accessory->qty = GETPOSTINT('qty') > 0 ? GETPOSTINT('qty') : $accessory->qty;
$accessory->note = GETPOST('note', 'alphanohtml');
$result = $accessory->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Speichern';
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'delete':
// Zubehör löschen
if (!$user->hasRight('kundenkarte', 'delete')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$id = GETPOSTINT('id');
if ($id > 0 && $accessory->fetch($id) > 0) {
$result = $accessory->delete($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Löschen';
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'order':
// Lieferantenbestellung aus Zubehör erstellen
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$anlageId = GETPOSTINT('fk_anlage');
$supplierId = GETPOSTINT('supplier_id');
$idsRaw = GETPOST('ids', 'array');
if ($anlageId > 0 && $supplierId > 0 && !empty($idsRaw)) {
$ids = array_map('intval', $idsRaw);
$result = $accessory->generateSupplierOrder($user, $supplierId, $anlageId, $ids);
if ($result > 0) {
$response['success'] = true;
$response['order_id'] = $result;
} else {
$response['error'] = $accessory->error ?: 'Fehler beim Erstellen der Bestellung';
}
} else {
$response['error'] = 'Fehlende Parameter (Anlage, Lieferant, IDs)';
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

71
ajax/busbar_types.php Executable file
View file

@ -0,0 +1,71 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for busbar types (Sammelschienen-Typen)
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/busbartype.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$systemId = GETPOSTINT('system_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$busbarType = new BusbarType($db);
switch ($action) {
case 'list':
// Get all busbar types for a system (or all if system_id = 0)
$types = $busbarType->fetchAllBySystem($systemId, 1);
$result = array();
foreach ($types as $t) {
$result[] = array(
'id' => $t->id,
'ref' => $t->ref,
'label' => $t->label,
'label_short' => $t->label_short,
'phases' => $t->phases,
'phases_config' => $t->phases_config ? json_decode($t->phases_config, true) : null,
'num_lines' => $t->num_lines,
'color' => $t->color,
'default_color' => $t->default_color,
'line_height' => $t->line_height,
'line_spacing' => $t->line_spacing,
'position_default' => $t->position_default,
'fk_system' => $t->fk_system,
'system_label' => $t->system_label
);
}
$response['success'] = true;
$response['types'] = $result;
break;
default:
$response['error'] = 'Unknown action';
break;
}
echo json_encode($response);

View file

@ -267,6 +267,7 @@ switch ($action) {
'block_color' => $eq->getBlockColor(),
'field_values' => $eq->getFieldValues(),
'fk_product' => $eq->fk_product,
'fk_protection' => $eq->fk_protection,
'product_ref' => $productRef,
'product_label' => $productLabel
);

View file

@ -44,8 +44,11 @@ switch ($action) {
'id' => $connection->id,
'fk_source' => $connection->fk_source,
'source_terminal' => $connection->source_terminal,
'source_terminal_id' => $connection->source_terminal_id,
'bundled_terminals' => $connection->bundled_terminals,
'fk_target' => $connection->fk_target,
'target_terminal' => $connection->target_terminal,
'target_terminal_id' => $connection->target_terminal_id,
'connection_type' => $connection->connection_type,
'color' => $connection->color,
'output_label' => $connection->output_label,
@ -77,11 +80,14 @@ switch ($action) {
'id' => $c->id,
'fk_source' => $c->fk_source,
'source_terminal' => $c->source_terminal,
'source_terminal_id' => $c->source_terminal_id,
'bundled_terminals' => $c->bundled_terminals,
'source_label' => $c->source_label,
'source_pos' => $c->source_pos,
'source_width' => $c->source_width,
'fk_target' => $c->fk_target,
'target_terminal' => $c->target_terminal,
'target_terminal_id' => $c->target_terminal_id,
'target_label' => $c->target_label,
'target_pos' => $c->target_pos,
'connection_type' => $c->connection_type,
@ -96,6 +102,8 @@ switch ($action) {
'rail_phases' => $c->rail_phases,
'excluded_te' => $c->excluded_te,
'position_y' => $c->position_y,
'fk_busbar_type' => $c->fk_busbar_type,
'phases_config' => $c->phases_config ? json_decode($c->phases_config, true) : null,
'display_label' => $c->getDisplayLabel()
);
}
@ -154,6 +162,7 @@ switch ($action) {
$connection->fk_carrier = $carrierId;
$connection->position_y = GETPOSTINT('position_y');
$connection->path_data = GETPOST('path_data', 'nohtml');
$connection->bundled_terminals = GETPOST('bundled_terminals', 'alphanohtml');
$result = $connection->create($user);
if ($result > 0) {
@ -178,6 +187,7 @@ switch ($action) {
if (GETPOSTISSET('connection_type')) $connection->connection_type = GETPOST('connection_type', 'alphanohtml');
if (GETPOSTISSET('color')) $connection->color = GETPOST('color', 'alphanohtml');
if (GETPOSTISSET('output_label')) $connection->output_label = GETPOST('output_label', 'alphanohtml');
if (GETPOSTISSET('output_location')) $connection->output_location = GETPOST('output_location', 'alphanohtml');
if (GETPOSTISSET('medium_type')) $connection->medium_type = GETPOST('medium_type', 'alphanohtml');
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
@ -188,6 +198,8 @@ switch ($action) {
if (GETPOSTISSET('excluded_te')) $connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
if (GETPOSTISSET('position_y')) $connection->position_y = GETPOSTINT('position_y');
if (GETPOSTISSET('path_data')) $connection->path_data = GETPOST('path_data', 'nohtml');
if (GETPOSTISSET('bundled_terminals')) $connection->bundled_terminals = GETPOST('bundled_terminals', 'alphanohtml');
if (GETPOSTISSET('fk_busbar_type')) $connection->fk_busbar_type = GETPOSTINT('fk_busbar_type') ?: null;
$result = $connection->update($user);
if ($result > 0) {
@ -231,6 +243,8 @@ switch ($action) {
$connection->rail_end_te = GETPOSTINT('rail_end_te');
$connection->rail_phases = GETPOST('rail_phases', 'alphanohtml');
$connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
$connection->num_lines = GETPOSTINT('num_lines') ?: 1;
$connection->fk_busbar_type = GETPOSTINT('fk_busbar_type') ?: null;
$connection->fk_carrier = $carrierId;
$connection->position_y = GETPOSTINT('position_y');
@ -240,6 +254,7 @@ switch ($action) {
$response['connection_id'] = $result;
} else {
$response['error'] = $connection->error ?: 'Create failed';
$response['sql_errors'] = $connection->errors;
}
break;
@ -289,6 +304,7 @@ switch ($action) {
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
$connection->color = GETPOST('color', 'alphanohtml');
$connection->output_label = GETPOST('output_label', 'alphanohtml');
$connection->output_location = GETPOST('output_location', 'alphanohtml');
$connection->medium_type = GETPOST('medium_type', 'alphanohtml');
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
@ -341,6 +357,7 @@ switch ($action) {
'fk_source' => $obj->fk_source,
'source_terminal' => $obj->source_terminal,
'source_terminal_id' => $obj->source_terminal_id,
'bundled_terminals' => isset($obj->bundled_terminals) ? $obj->bundled_terminals : null,
'source_label' => $obj->source_label,
'source_pos' => $obj->source_pos,
'source_width' => $obj->source_width,
@ -352,6 +369,7 @@ switch ($action) {
'connection_type' => $obj->connection_type,
'color' => $obj->color ?: '#3498db',
'output_label' => $obj->output_label,
'output_location' => isset($obj->output_location) ? $obj->output_location : null,
'medium_type' => $obj->medium_type,
'medium_spec' => $obj->medium_spec,
'medium_length' => $obj->medium_length,
@ -359,6 +377,8 @@ switch ($action) {
'rail_start_te' => $obj->rail_start_te,
'rail_end_te' => $obj->rail_end_te,
'rail_phases' => $obj->rail_phases,
'num_lines' => isset($obj->num_lines) ? $obj->num_lines : 1,
'fk_busbar_type' => isset($obj->fk_busbar_type) ? $obj->fk_busbar_type : null,
'position_y' => $obj->position_y,
'fk_carrier' => $obj->fk_carrier,
'path_data' => isset($obj->path_data) ? $obj->path_data : null

View file

@ -0,0 +1,60 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Leitungslaufplan PDF-Export (Stromlaufplan in aufgelöster Darstellung)
* Separater Endpoint - kann ohne Auswirkungen entfernt werden.
*/
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/lib/wiring_diagram.lib.php');
$langs->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');

View file

@ -66,7 +66,7 @@ if ($resFields) {
// Elemente laden - OHNE GLOBAL-System (das ist nur die separate Gebäudestruktur)
// Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL)
// Hierarchie kommt aus fk_parent (wie im Baum)
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type,";
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type, a.decommissioned, a.date_decommissioned,";
$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y, a.graph_width, a.graph_height,";
$sql .= " t.label as type_label, t.picto as type_picto, t.color as type_color,";
$sql .= " t.can_have_children as type_can_have_children,";
@ -128,6 +128,8 @@ if ($resql) {
'fk_parent' => (int) $obj->fk_parent,
'fk_anlage_type' => (int) $obj->fk_anlage_type,
'is_building' => $isBuilding,
'decommissioned' => isset($obj->decommissioned) ? (int) $obj->decommissioned : 0,
'date_decommissioned' => isset($obj->date_decommissioned) ? $obj->date_decommissioned : null,
'image_count' => (int) $obj->image_count,
'doc_count' => (int) $obj->doc_count,
'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null,

74
ajax/pwa_api.php Normal file → Executable file
View file

@ -291,7 +291,7 @@ switch ($action) {
$outputsData = array();
if (!empty($equipmentData)) {
$equipmentIds = array_map(function($e) { return (int) $e['id']; }, $equipmentData);
$sql = "SELECT rowid, fk_source, output_label, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id, bundled_terminals";
$sql = "SELECT rowid, fk_source, output_label, output_location, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id, bundled_terminals";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
$sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")";
$sql .= " AND fk_target IS NULL";
@ -344,6 +344,7 @@ switch ($action) {
'id' => $obj->rowid,
'fk_source' => $obj->fk_source,
'output_label' => $obj->output_label,
'output_location' => isset($obj->output_location) ? $obj->output_location : '',
'medium_type' => $obj->medium_type,
'medium_spec' => $obj->medium_spec,
'medium_length' => $obj->medium_length,
@ -360,7 +361,7 @@ switch ($action) {
// Einspeisungen laden (Connections mit fk_source IS NULL = Inputs)
$inputsData = array();
if (!empty($equipmentData)) {
$sql = "SELECT rowid, fk_target, output_label, connection_type, color";
$sql = "SELECT rowid, fk_target, target_terminal_id, output_label, connection_type, color";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
$sql .= " WHERE fk_target IN (".implode(',', $equipmentIds).")";
$sql .= " AND fk_source IS NULL";
@ -371,6 +372,7 @@ switch ($action) {
$inputsData[] = array(
'id' => $obj->rowid,
'fk_target' => $obj->fk_target,
'target_terminal_id' => $obj->target_terminal_id ?: '',
'output_label' => $obj->output_label,
'connection_type' => $obj->connection_type,
'color' => $obj->color
@ -379,31 +381,64 @@ switch ($action) {
}
}
// Verbindungen zwischen Equipment laden (mit path_data für Linien-Anzeige)
// Verbindungen zwischen Equipment laden (alle, für Phasen-Propagierung + Linien-Anzeige)
$connectionsData = array();
if (!empty($equipmentData)) {
$sql = "SELECT rowid, fk_source, fk_target, source_terminal_id, target_terminal_id, connection_type, color, path_data";
$sql = "SELECT rowid, fk_source, fk_target, source_terminal_id, target_terminal_id, connection_type, color, path_data, is_rail";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
$sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")";
$sql .= " AND fk_target IS NOT NULL";
$sql .= " AND fk_target IN (".implode(',', $equipmentIds).")";
$sql .= " AND is_rail = 0";
$sql .= " AND status = 1";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
// Nur Verbindungen mit gezeichnetem Pfad laden
if (!empty($obj->path_data)) {
$connectionsData[] = array(
'id' => $obj->rowid,
'fk_source' => $obj->fk_source,
'fk_target' => $obj->fk_target,
'source_terminal_id' => $obj->source_terminal_id,
'target_terminal_id' => $obj->target_terminal_id,
'connection_type' => $obj->connection_type,
'color' => $obj->color,
'path_data' => $obj->path_data
);
$connectionsData[] = array(
'id' => $obj->rowid,
'fk_source' => $obj->fk_source,
'fk_target' => $obj->fk_target,
'source_terminal_id' => $obj->source_terminal_id,
'target_terminal_id' => $obj->target_terminal_id,
'connection_type' => $obj->connection_type,
'color' => $obj->color,
'path_data' => $obj->path_data ?: null
);
}
}
}
// Busbars (Phasenschienen) laden für Farbpropagierung
$busbarsData = array();
if (!empty($carriersData)) {
$carrierIds = array_map(function($c) { return (int) $c['id']; }, $carriersData);
$sql = "SELECT c.rowid, c.fk_carrier, c.rail_start_te, c.rail_end_te, c.rail_phases,";
$sql .= " c.excluded_te, c.position_y, c.connection_type,";
$sql .= " bt.phases_config as busbar_phases_config";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection as c";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type as bt ON c.fk_busbar_type = bt.rowid";
$sql .= " WHERE c.fk_carrier IN (".implode(',', $carrierIds).")";
$sql .= " AND c.is_rail = 1";
$sql .= " AND c.status = 1";
$sql .= " ORDER BY c.position_y ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$phasesConfig = null;
if (!empty($obj->busbar_phases_config)) {
$phasesConfig = json_decode($obj->busbar_phases_config, true);
}
$busbarsData[] = array(
'id' => $obj->rowid,
'fk_carrier' => $obj->fk_carrier,
'rail_start_te' => (int) $obj->rail_start_te,
'rail_end_te' => (int) $obj->rail_end_te,
'rail_phases' => $obj->rail_phases,
'excluded_te' => $obj->excluded_te ?: '',
'position_y' => (int) $obj->position_y,
'connection_type' => $obj->connection_type,
'phases_config' => $phasesConfig
);
}
}
}
@ -458,6 +493,7 @@ switch ($action) {
$response['outputs'] = $outputsData;
$response['inputs'] = $inputsData;
$response['connections'] = $connectionsData;
$response['busbars'] = $busbarsData;
$response['types'] = $typesData;
$response['field_meta'] = $fieldMetaData;
break;
@ -481,7 +517,7 @@ switch ($action) {
$panel = new EquipmentPanel($db);
$panel->fk_anlage = $anlageId;
$panel->label = $label ?: 'Feld';
$panel->label = $label; // PHP-Klasse vergibt Auto-Name wenn leer
$result = $panel->create($user);
if ($result > 0) {
@ -520,7 +556,7 @@ switch ($action) {
$carrier = new EquipmentCarrier($db);
$carrier->fk_anlage = $panelObj->fk_anlage;
$carrier->fk_panel = $panelId;
$carrier->label = $label ?: 'Hutschiene';
$carrier->label = $label; // PHP-Klasse vergibt Auto-Name wenn leer
$carrier->total_te = $totalTe;
$result = $carrier->create($user);
@ -837,6 +873,7 @@ switch ($action) {
$conn->connection_type = $connectionType;
$conn->color = GETPOST('color', 'alphanohtml');
$conn->output_label = $outputLabel;
$conn->output_location = GETPOST('output_location', 'alphanohtml');
$conn->fk_carrier = $eq->fk_carrier;
if ($direction === 'input') {
@ -892,6 +929,7 @@ switch ($action) {
$conn->connection_type = GETPOST('connection_type', 'alphanohtml');
$conn->color = GETPOST('color', 'alphanohtml');
$conn->output_label = GETPOST('output_label', 'alphanohtml');
if (GETPOSTISSET('output_location')) $conn->output_location = GETPOST('output_location', 'alphanohtml');
$conn->medium_type = GETPOST('medium_type', 'alphanohtml');
$conn->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$conn->medium_length = GETPOST('medium_length', 'alphanohtml');

View file

@ -24,6 +24,7 @@ class Anlage extends CommonObject
public $fk_parent;
public $fk_system;
public $fk_building_node;
public $fk_product;
public $manufacturer;
public $model;
@ -40,6 +41,8 @@ class Anlage extends CommonObject
public $note_private;
public $note_public;
public $status;
public $decommissioned;
public $date_decommissioned;
public $date_creation;
public $fk_user_creat;
@ -94,7 +97,7 @@ class Anlage extends CommonObject
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, fk_soc, fk_contact, fk_anlage_type, fk_parent, fk_system,";
$sql .= "entity, ref, label, fk_soc, fk_contact, fk_anlage_type, fk_parent, fk_system, fk_product,";
$sql .= " manufacturer, model, serial_number, power_rating, field_values,";
$sql .= " location, installation_date, warranty_until,";
$sql .= " rang, level, note_private, note_public, status,";
@ -108,6 +111,7 @@ class Anlage extends CommonObject
$sql .= ", ".((int) $this->fk_anlage_type);
$sql .= ", ".((int) ($this->fk_parent > 0 ? $this->fk_parent : 0));
$sql .= ", ".((int) $this->fk_system);
$sql .= ", ".($this->fk_product > 0 ? (int) $this->fk_product : "NULL");
$sql .= ", ".($this->manufacturer ? "'".$this->db->escape($this->manufacturer)."'" : "NULL");
$sql .= ", ".($this->model ? "'".$this->db->escape($this->model)."'" : "NULL");
$sql .= ", ".($this->serial_number ? "'".$this->db->escape($this->serial_number)."'" : "NULL");
@ -201,6 +205,7 @@ class Anlage extends CommonObject
$this->fk_parent = $obj->fk_parent;
$this->fk_system = $obj->fk_system;
$this->fk_building_node = isset($obj->fk_building_node) ? (int) $obj->fk_building_node : 0;
$this->fk_product = isset($obj->fk_product) ? (int) $obj->fk_product : null;
$this->manufacturer = $obj->manufacturer;
$this->model = $obj->model;
@ -217,6 +222,8 @@ class Anlage extends CommonObject
$this->note_private = $obj->note_private;
$this->note_public = $obj->note_public;
$this->status = $obj->status;
$this->decommissioned = isset($obj->decommissioned) ? (int) $obj->decommissioned : 0;
$this->date_decommissioned = isset($obj->date_decommissioned) ? $obj->date_decommissioned : null;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->tms = $this->db->jdate($obj->tms);
@ -227,8 +234,14 @@ class Anlage extends CommonObject
$this->type_label = $obj->type_label;
$this->type_short = $obj->type_short;
$this->type_picto = $obj->type_picto;
$this->type_color = isset($obj->type_color) ? $obj->type_color : '';
$this->type_can_have_children = isset($obj->type_can_have_children) ? (int) $obj->type_can_have_children : 0;
$this->type_can_have_equipment = isset($obj->type_can_have_equipment) ? (int) $obj->type_can_have_equipment : 0;
$this->type_has_accessories = isset($obj->type_has_accessories) ? (int) $obj->type_has_accessories : 0;
// Produkt-Info (aus JOIN)
$this->product_ref = isset($obj->product_ref) ? $obj->product_ref : '';
$this->product_label = isset($obj->product_label) ? $obj->product_label : '';
// System info
$this->system_label = $obj->system_label;
@ -287,6 +300,9 @@ class Anlage extends CommonObject
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
$sql .= ", status = ".((int) $this->status);
$sql .= ", fk_product = ".($this->fk_product > 0 ? (int) $this->fk_product : "NULL");
$sql .= ", decommissioned = ".((int) $this->decommissioned);
$sql .= ", date_decommissioned = ".($this->date_decommissioned ? "'".$this->db->escape($this->date_decommissioned)."'" : "NULL");
$sql .= ", fk_user_modif = ".((int) $user->id);
$sql .= " WHERE rowid = ".((int) $this->id);
@ -401,9 +417,11 @@ class Anlage extends CommonObject
$results = array();
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,";
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto, t.color as type_color,";
$sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,";
$sql .= " t.has_accessories as type_has_accessories,";
$sql .= " s.label as system_label, s.code as system_code,";
$sql .= " p.ref as product_ref, p.label as product_label,";
// Count images
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
// Count documents (pdf + document)
@ -411,6 +429,7 @@ class Anlage extends CommonObject
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_parent = ".((int) $parentId);
$sql .= " AND a.entity = ".((int) $conf->entity);
$sql .= " AND a.status = 1";
@ -711,9 +730,11 @@ class Anlage extends CommonObject
$results = array();
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,";
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto, t.color as type_color,";
$sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,";
$sql .= " t.has_accessories as type_has_accessories,";
$sql .= " s.label as system_label, s.code as system_code,";
$sql .= " p.ref as product_ref, p.label as product_label,";
// Count images
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
// Count documents (pdf + document)
@ -721,6 +742,7 @@ class Anlage extends CommonObject
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_parent = ".((int) $parentId);
$sql .= " AND a.entity = ".((int) $conf->entity);
$sql .= " AND a.status = 1";

391
class/anlageaccessory.class.php Executable file
View file

@ -0,0 +1,391 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class AnlageAccessory
* Verwaltet Zubehör/Ersatzteile für Anlagen-Elemente
*/
class AnlageAccessory extends CommonObject
{
public $element = 'anlageaccessory';
public $table_element = 'kundenkarte_anlage_accessory';
public $fk_anlage;
public $fk_product;
public $qty;
public $rang;
public $note;
public $date_creation;
public $fk_user_creat;
// Geladene Objekte (aus JOIN)
public $product_ref;
public $product_label;
public $product_price;
public $product_fk_unit;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Zubehör erstellen
*
* @param User $user Benutzer
* @return int <0 bei Fehler, ID bei Erfolg
*/
public function create($user)
{
$error = 0;
$now = dol_now();
if (empty($this->fk_anlage) || empty($this->fk_product)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
// Prüfen ob bereits vorhanden
if ($this->alreadyExists($this->fk_anlage, $this->fk_product)) {
$this->error = 'ErrorAccessoryAlreadyExists';
return -2;
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "fk_anlage, fk_product, qty, rang, note,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $this->fk_anlage);
$sql .= ", ".((int) $this->fk_product);
$sql .= ", ".((float) ($this->qty > 0 ? $this->qty : 1));
$sql .= ", ".((int) $this->rang);
$sql .= ", ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $user->id);
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if (!$error) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return $this->id;
}
}
/**
* Zubehör laden
*
* @param int $id ID
* @return int <0 bei Fehler, 0 nicht gefunden, >0 OK
*/
public function fetch($id)
{
$sql = "SELECT a.*, p.ref as product_ref, p.label as product_label, p.price as product_price, p.fk_unit as product_fk_unit";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->fk_anlage = $obj->fk_anlage;
$this->fk_product = $obj->fk_product;
$this->qty = $obj->qty;
$this->rang = $obj->rang;
$this->note = $obj->note;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
$this->product_price = $obj->product_price;
$this->product_fk_unit = $obj->product_fk_unit;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Zubehör aktualisieren
*
* @param User $user Benutzer
* @return int <0 bei Fehler, >0 OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " qty = ".((float) $this->qty);
$sql .= ", rang = ".((int) $this->rang);
$sql .= ", note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Zubehör löschen
*
* @param User $user Benutzer
* @return int <0 bei Fehler, >0 OK
*/
public function delete($user)
{
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$this->db->rollback();
$this->error = $this->db->lasterror();
return -1;
}
$this->db->commit();
return 1;
}
/**
* Alle Zubehörteile einer Anlage laden
*
* @param int $anlageId Anlage-ID
* @return array Array von AnlageAccessory-Objekten
*/
public function fetchAllByAnlage($anlageId)
{
$results = array();
$sql = "SELECT a.*, p.ref as product_ref, p.label as product_label, p.price as product_price, p.fk_unit as product_fk_unit";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_anlage = ".((int) $anlageId);
$sql .= " ORDER BY a.rang ASC, a.rowid ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$acc = new AnlageAccessory($this->db);
$acc->id = $obj->rowid;
$acc->fk_anlage = $obj->fk_anlage;
$acc->fk_product = $obj->fk_product;
$acc->qty = $obj->qty;
$acc->rang = $obj->rang;
$acc->note = $obj->note;
$acc->date_creation = $this->db->jdate($obj->date_creation);
$acc->fk_user_creat = $obj->fk_user_creat;
$acc->product_ref = $obj->product_ref;
$acc->product_label = $obj->product_label;
$acc->product_price = $obj->product_price;
$acc->product_fk_unit = $obj->product_fk_unit;
$results[] = $acc;
}
$this->db->free($resql);
}
return $results;
}
/**
* Prüfen ob Produkt bereits als Zubehör zugeordnet ist
*
* @param int $anlageId Anlage-ID
* @param int $productId Produkt-ID
* @return bool true wenn bereits vorhanden
*/
public function alreadyExists($anlageId, $productId)
{
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
$sql .= " AND fk_product = ".((int) $productId);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
return ($obj->cnt > 0);
}
return false;
}
/**
* Lieferantenbestellung aus ausgewählten Zubehörteilen erstellen
*
* @param User $user Benutzer
* @param int $supplierId Lieferanten-ID (fournisseur)
* @param int $anlageId Anlage-ID
* @param array $selectedIds Array von Accessory-IDs
* @param array $quantities Optional: ID => Menge
* @return int Bestell-ID bei Erfolg, <0 bei Fehler
*/
public function generateSupplierOrder($user, $supplierId, $anlageId, $selectedIds, $quantities = array())
{
global $conf, $langs, $mysoc;
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
if (empty($selectedIds)) {
$this->error = 'NoProductsSelected';
return -1;
}
// Lieferant laden
$supplier = new Societe($this->db);
if ($supplier->fetch($supplierId) <= 0) {
$this->error = 'ErrorLoadingSupplier';
return -1;
}
// Zubehör der Anlage laden
$accessories = $this->fetchAllByAnlage($anlageId);
if (!is_array($accessories) || empty($accessories)) {
$this->error = 'NoAccessoriesFound';
return -2;
}
// Ausgewählte filtern
$toAdd = array();
foreach ($accessories as $acc) {
if (in_array($acc->id, $selectedIds)) {
$qty = isset($quantities[$acc->id]) ? (float) $quantities[$acc->id] : $acc->qty;
if ($qty > 0) {
$toAdd[] = array(
'product_id' => $acc->fk_product,
'qty' => $qty
);
}
}
}
if (empty($toAdd)) {
$this->error = 'NoValidProductsToAdd';
return -2;
}
// Lieferantenbestellung erstellen
$order = new CommandeFournisseur($this->db);
$order->socid = $supplierId;
$order->date = dol_now();
$order->note_private = $langs->trans('OrderGeneratedFromAccessories');
$this->db->begin();
$result = $order->create($user);
if ($result <= 0) {
$this->error = $order->error;
$this->errors = $order->errors;
$this->db->rollback();
return -3;
}
// Produkte hinzufügen
foreach ($toAdd as $item) {
$product = new Product($this->db);
$product->fetch($item['product_id']);
// MwSt-Satz ermitteln (Lieferant = Verkäufer, eigene Firma = Käufer)
$tva_tx = get_default_tva($supplier, $mysoc, $product->id);
$localtax1_tx = get_default_localtax($supplier, $mysoc, 1, $product->id);
$localtax2_tx = get_default_localtax($supplier, $mysoc, 2, $product->id);
// Lieferantenpreis ermitteln
$fournPrice = $product->price;
$fournPriceId = 0;
$fournRef = '';
$sqlFourn = "SELECT rowid, price as fourn_price, ref_fourn";
$sqlFourn .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlFourn .= " WHERE fk_product = ".((int) $product->id);
$sqlFourn .= " AND fk_soc = ".((int) $supplierId);
$sqlFourn .= " ORDER BY price ASC LIMIT 1";
$resFourn = $this->db->query($sqlFourn);
if ($resFourn && $this->db->num_rows($resFourn) > 0) {
$objFourn = $this->db->fetch_object($resFourn);
$fournPrice = $objFourn->fourn_price;
$fournPriceId = $objFourn->rowid;
$fournRef = $objFourn->ref_fourn;
}
$lineResult = $order->addline(
$product->label, // Beschreibung
$fournPrice, // Preis HT
$item['qty'], // Menge
$tva_tx, // MwSt
$localtax1_tx, // Lokale Steuer 1
$localtax2_tx, // Lokale Steuer 2
$product->id, // Produkt-ID
$fournPriceId, // Lieferantenpreis-ID
$fournRef, // Lieferanten-Referenz
0, // Rabatt
'HT', // Preis-Basis
0, // Preis TTC
0, // Typ (0=Produkt)
0, // Info bits
false, // notrigger
null, // Startdatum
null, // Enddatum
array(), // Optionen
$product->fk_unit // Einheit
);
if ($lineResult < 0) {
$this->error = $order->error;
$this->errors = $order->errors;
$this->db->rollback();
return -4;
}
}
$this->db->commit();
return $order->id;
}
}

View file

@ -26,6 +26,8 @@ class AnlageType extends CommonObject
public $can_be_nested;
public $allowed_parent_types;
public $can_have_equipment;
public $has_accessories;
public $has_product;
public $picto;
public $color;
@ -73,7 +75,7 @@ class AnlageType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment,";
$sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment, has_accessories, has_product,";
$sql .= " picto, color, is_system, position, active,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
@ -87,6 +89,8 @@ class AnlageType extends CommonObject
$sql .= ", ".((int) $this->can_be_nested);
$sql .= ", ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
$sql .= ", ".((int) $this->can_have_equipment);
$sql .= ", ".((int) $this->has_accessories);
$sql .= ", ".((int) $this->has_product);
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created
@ -144,6 +148,8 @@ class AnlageType extends CommonObject
$this->can_be_nested = $obj->can_be_nested;
$this->allowed_parent_types = $obj->allowed_parent_types;
$this->can_have_equipment = $obj->can_have_equipment ?? 0;
$this->has_accessories = $obj->has_accessories ?? 0;
$this->has_product = $obj->has_product ?? 0;
$this->picto = $obj->picto;
$this->color = $obj->color;
$this->is_system = $obj->is_system;
@ -190,6 +196,8 @@ class AnlageType extends CommonObject
$sql .= ", can_be_nested = ".((int) $this->can_be_nested);
$sql .= ", allowed_parent_types = ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
$sql .= ", can_have_equipment = ".((int) $this->can_have_equipment);
$sql .= ", has_accessories = ".((int) $this->has_accessories);
$sql .= ", has_product = ".((int) $this->has_product);
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", position = ".((int) $this->position);
@ -312,7 +320,10 @@ class AnlageType extends CommonObject
$type->can_be_nested = $obj->can_be_nested;
$type->allowed_parent_types = $obj->allowed_parent_types;
$type->can_have_equipment = $obj->can_have_equipment ?? 0;
$type->has_accessories = $obj->has_accessories ?? 0;
$type->has_product = $obj->has_product ?? 0;
$type->picto = $obj->picto;
$type->color = $obj->color;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;

View file

@ -33,6 +33,7 @@ class BuildingType extends CommonObject
public $picto;
public $is_system;
public $can_have_children;
public $has_product;
public $position;
public $active;
public $date_creation;
@ -69,7 +70,7 @@ class BuildingType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_parent, level_type,";
$sql .= "icon, color, picto, is_system, can_have_children, position, active,";
$sql .= "icon, color, picto, is_system, can_have_children, has_product, position, active,";
$sql .= "date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= (int)$conf->entity;
@ -84,6 +85,7 @@ class BuildingType extends CommonObject
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".(int)($this->is_system ?: 0);
$sql .= ", ".(int)($this->can_have_children !== null ? $this->can_have_children : 1);
$sql .= ", ".(int)($this->has_product ?: 0);
$sql .= ", ".(int)($this->position ?: 0);
$sql .= ", ".(int)($this->active !== null ? $this->active : 1);
$sql .= ", '".$this->db->idate($now)."'";
@ -135,6 +137,7 @@ class BuildingType extends CommonObject
$this->picto = $obj->picto;
$this->is_system = $obj->is_system;
$this->can_have_children = $obj->can_have_children;
$this->has_product = $obj->has_product ?? 0;
$this->position = $obj->position;
$this->active = $obj->active;
$this->date_creation = $this->db->jdate($obj->date_creation);
@ -171,6 +174,7 @@ class BuildingType extends CommonObject
$sql .= ", icon = ".($this->icon ? "'".$this->db->escape($this->icon)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", can_have_children = ".(int)$this->can_have_children;
$sql .= ", has_product = ".(int)($this->has_product ?: 0);
$sql .= ", position = ".(int)$this->position;
$sql .= ", active = ".(int)$this->active;
$sql .= ", fk_user_modif = ".(int)$user->id;
@ -265,6 +269,7 @@ class BuildingType extends CommonObject
$type->picto = $obj->picto;
$type->is_system = $obj->is_system;
$type->can_have_children = $obj->can_have_children;
$type->has_product = $obj->has_product ?? 0;
$type->position = $obj->position;
$type->active = $obj->active;
$type->parent_label = $obj->parent_label;

View file

@ -24,6 +24,7 @@ class BusbarType extends CommonObject
// Busbar-spezifische Felder
public $phases; // Channel configuration (A, B, AB, ABC, or legacy L1, L2, L3, N, PE, etc.)
public $phases_config; // JSON array of phase labels per line, e.g. ["L1","L2","L3"]
public $num_lines = 1; // Anzahl der Linien
public $color; // Kommagetrennte Farben
public $default_color; // Standard-Einzelfarbe
@ -80,7 +81,7 @@ class BusbarType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " phases, num_lines, color, default_color, line_height, line_spacing, position_default,";
$sql .= " phases, phases_config, num_lines, color, default_color, line_height, line_spacing, position_default,";
$sql .= " fk_product, picto, icon_file, is_system, position, active,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
@ -91,6 +92,7 @@ class BusbarType extends CommonObject
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", ".((int) $this->fk_system);
$sql .= ", '".$this->db->escape($this->phases)."'";
$sql .= ", ".($this->phases_config ? "'".$this->db->escape($this->phases_config)."'" : "NULL");
$sql .= ", ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
@ -206,6 +208,7 @@ class BusbarType extends CommonObject
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", fk_system = ".((int) $this->fk_system);
$sql .= ", phases = '".$this->db->escape($this->phases)."'";
$sql .= ", phases_config = ".($this->phases_config ? "'".$this->db->escape($this->phases_config)."'" : "NULL");
$sql .= ", num_lines = ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", default_color = ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
@ -316,6 +319,7 @@ class BusbarType extends CommonObject
$type->label_short = $obj->label_short;
$type->fk_system = $obj->fk_system;
$type->phases = $obj->phases;
$type->phases_config = $obj->phases_config;
$type->num_lines = $obj->num_lines;
$type->color = $obj->color;
$type->default_color = $obj->default_color;

View file

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

View file

@ -30,6 +30,7 @@ class EquipmentConnection extends CommonObject
// Output/endpoint info
public $output_label;
public $output_location; // Räumlichkeit/Örtlichkeit des Verbrauchers
// Medium info (cable, wire, etc.)
public $medium_type;
@ -42,6 +43,9 @@ class EquipmentConnection extends CommonObject
public $rail_end_te;
public $rail_phases; // '3P', '3P+N', 'L1', 'L1N', etc.
public $excluded_te; // Comma-separated TE positions to exclude (gaps for FI)
public $num_lines = 1; // Number of lines for busbar (1-5)
public $fk_busbar_type; // Reference to busbar type template
public $phases_config; // JSON array of phase labels from busbar type
public $fk_carrier;
public $position_y = 0;
@ -88,9 +92,9 @@ class EquipmentConnection extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, fk_source, source_terminal, source_terminal_id, bundled_terminals, fk_target, target_terminal, target_terminal_id,";
$sql .= " connection_type, color, output_label,";
$sql .= " connection_type, color, output_label, output_location,";
$sql .= " medium_type, medium_spec, medium_length,";
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y, path_data,";
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, num_lines, fk_busbar_type, fk_carrier, position_y, path_data,";
$sql .= " note_private, status, date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $conf->entity);
@ -104,6 +108,7 @@ class EquipmentConnection extends CommonObject
$sql .= ", ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
$sql .= ", ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL");
$sql .= ", ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
$sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
$sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
@ -112,6 +117,8 @@ class EquipmentConnection extends CommonObject
$sql .= ", ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL");
$sql .= ", ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL");
$sql .= ", ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
$sql .= ", ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
$sql .= ", ".($this->fk_busbar_type > 0 ? ((int) $this->fk_busbar_type) : "NULL");
$sql .= ", ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
$sql .= ", ".((int) $this->position_y);
$sql .= ", ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
@ -173,6 +180,7 @@ class EquipmentConnection extends CommonObject
$this->connection_type = $obj->connection_type;
$this->color = $obj->color;
$this->output_label = $obj->output_label;
$this->output_location = isset($obj->output_location) ? $obj->output_location : null;
$this->medium_type = $obj->medium_type;
$this->medium_spec = $obj->medium_spec;
$this->medium_length = $obj->medium_length;
@ -229,6 +237,7 @@ class EquipmentConnection extends CommonObject
$sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
$sql .= ", output_location = ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL");
$sql .= ", medium_type = ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
$sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
$sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
@ -237,6 +246,7 @@ class EquipmentConnection extends CommonObject
$sql .= ", rail_end_te = ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL");
$sql .= ", rail_phases = ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL");
$sql .= ", excluded_te = ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
$sql .= ", fk_busbar_type = ".($this->fk_busbar_type > 0 ? ((int) $this->fk_busbar_type) : "NULL");
$sql .= ", fk_carrier = ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
$sql .= ", position_y = ".((int) $this->position_y);
$sql .= ", path_data = ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
@ -300,10 +310,12 @@ class EquipmentConnection extends CommonObject
$sql = "SELECT c.*, ";
$sql .= " src.label as source_label, src.position_te as source_pos, src.width_te as source_width,";
$sql .= " tgt.label as target_label, tgt.position_te as target_pos";
$sql .= " tgt.label as target_label, tgt.position_te as target_pos,";
$sql .= " bt.phases_config as busbar_phases_config";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type as bt ON c.fk_busbar_type = bt.rowid";
$sql .= " WHERE c.fk_carrier = ".((int) $carrierId);
if ($activeOnly) {
$sql .= " AND c.status = 1";
@ -344,6 +356,8 @@ class EquipmentConnection extends CommonObject
$conn->source_width = $obj->source_width;
$conn->target_label = $obj->target_label;
$conn->target_pos = $obj->target_pos;
$conn->fk_busbar_type = isset($obj->fk_busbar_type) ? $obj->fk_busbar_type : null;
$conn->phases_config = isset($obj->busbar_phases_config) ? $obj->busbar_phases_config : null;
$results[] = $conn;
}

View file

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

View file

@ -328,6 +328,7 @@ class EquipmentType extends CommonObject
$type->picto = $obj->picto;
$type->icon_file = $obj->icon_file;
$type->block_image = $obj->block_image;
$type->terminals_config = $obj->terminals_config;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;

View file

@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '8.3';
$this->version = '11.1.2';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -412,6 +412,23 @@ class modKundenKarte extends DolibarrModules
'target' => '',
'user' => 0,
);
// Werkzeuge-Seite unter Start-Menü
$this->menu[$r++] = array(
'fk_menu' => 'fk_mainmenu=home',
'type' => 'left',
'titre' => 'CompanyTools',
'prefix' => img_picto('', 'fa-wrench', 'class="pictofixedwidth valignmiddle paddingright"'),
'mainmenu' => 'home',
'leftmenu' => 'kundenkarte_werkzeuge',
'url' => '/kundenkarte/werkzeuge.php',
'langs' => 'kundenkarte@kundenkarte',
'position' => 100,
'enabled' => 'isModEnabled("kundenkarte")',
'perms' => '$user->hasRight("kundenkarte", "read")',
'target' => '',
'user' => 0,
);
/* END MODULEBUILDER LEFTMENU */
@ -633,6 +650,21 @@ class modKundenKarte extends DolibarrModules
// v6.8.0: Schutzgruppen-Zuordnung (fk_protection)
$this->migrate_v680_protection_groups();
// v8.0.0: Ausgebaut-Status für Anlagen
$this->migrate_v800_decommissioned();
// v8.1.0: Werkzeuge & Zubehör
$this->migrate_v810_werkzeuge();
// v8.6.0: has_product Flag für Typen
$this->migrate_v860_has_product();
// v11.0.0: Busbar type reference and num_lines for connections
$this->migrate_v1100_busbar_fields();
// v11.1.0: Räumlichkeit (output_location) für Abgänge
$this->migrate_v1110_output_location();
}
/**
@ -927,6 +959,161 @@ class modKundenKarte extends DolibarrModules
}
}
/**
* Migration v8.0.0: Ausgebaut-Status für Anlagen
*/
private function migrate_v800_decommissioned()
{
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'decommissioned'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN decommissioned tinyint DEFAULT 0 NOT NULL AFTER status");
$this->db->query("ALTER TABLE ".$table." ADD INDEX idx_anlage_decommissioned (decommissioned)");
}
// Ausbau-Datum
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'date_decommissioned'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN date_decommissioned DATE NULL AFTER decommissioned");
}
}
/**
* Migration v8.1.0: Werkzeuge & Zubehör
* - fk_product auf Anlage (Produkt-Zuordnung)
* - has_accessories auf Anlage-Typ
* - Zubehör-Tabelle
* - WERKZEUG System-Kategorie
*/
private function migrate_v810_werkzeuge()
{
// 1. fk_product auf Anlage
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_product'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_product integer NULL AFTER fk_building_node");
$this->db->query("ALTER TABLE ".$table." ADD INDEX idx_anlage_fk_product (fk_product)");
}
// 2. has_accessories auf Anlage-Typ
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'has_accessories'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN has_accessories tinyint DEFAULT 0 NOT NULL AFTER can_have_equipment");
}
// 3. Zubehör-Tabelle
$table = MAIN_DB_PREFIX."kundenkarte_anlage_accessory";
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$sql = "CREATE TABLE ".$table." (";
$sql .= " rowid integer AUTO_INCREMENT PRIMARY KEY,";
$sql .= " fk_anlage integer NOT NULL,";
$sql .= " fk_product integer NOT NULL,";
$sql .= " qty double DEFAULT 1,";
$sql .= " rang integer DEFAULT 0,";
$sql .= " note varchar(255),";
$sql .= " date_creation datetime,";
$sql .= " fk_user_creat integer,";
$sql .= " UNIQUE KEY uk_anlage_accessory (fk_anlage, fk_product),";
$sql .= " INDEX idx_accessory_anlage (fk_anlage),";
$sql .= " CONSTRAINT fk_accessory_anlage FOREIGN KEY (fk_anlage) REFERENCES ".MAIN_DB_PREFIX."kundenkarte_anlage(rowid) ON DELETE CASCADE";
$sql .= ") ENGINE=InnoDB";
$this->db->query($sql);
}
// 4. WERKZEUG System-Kategorie (falls nicht vorhanden)
$sysTable = MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
$resql = $this->db->query("SELECT rowid FROM ".$sysTable." WHERE code = 'WERKZEUG'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("INSERT INTO ".$sysTable." (code, label, active, position) VALUES ('WERKZEUG', 'Werkzeuge & Maschinen', 1, 90)");
}
}
/**
* Migration v8.6.0: has_product Flag für Element-Typen und Gebäude-Typen
*/
private function migrate_v860_has_product()
{
// 1. has_product auf Element-Typ
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'has_product'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN has_product tinyint DEFAULT 0 NOT NULL AFTER has_accessories");
}
// 2. has_product auf Gebäude-Typ
$table = MAIN_DB_PREFIX."kundenkarte_building_type";
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
if ($resql && $this->db->num_rows($resql) > 0) {
$resql2 = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'has_product'");
if (!$resql2 || $this->db->num_rows($resql2) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN has_product tinyint DEFAULT 0 NOT NULL AFTER can_have_children");
}
}
}
/**
* Migration v11.0.0: Add busbar type fields to connections
*/
private function migrate_v1100_busbar_fields()
{
$table = MAIN_DB_PREFIX."kundenkarte_equipment_connection";
// Check if table exists
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
if (!$resql || $this->db->num_rows($resql) == 0) {
return;
}
// Add num_lines column
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'num_lines'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN num_lines int(11) DEFAULT 1 AFTER excluded_te");
}
// Add fk_busbar_type column
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_busbar_type'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_busbar_type int(11) DEFAULT NULL AFTER num_lines");
}
// Extend color column to support comma-separated colors for multi-line busbars
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." WHERE Field = 'color' AND Type LIKE 'varchar(20)%'");
if ($resql && $this->db->num_rows($resql) > 0) {
$this->db->query("ALTER TABLE ".$table." MODIFY COLUMN color varchar(255)");
}
// Add phases_config column to busbar_type table
$busbarTable = MAIN_DB_PREFIX."kundenkarte_busbar_type";
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($busbarTable)."'");
if ($resql && $this->db->num_rows($resql) > 0) {
$resql2 = $this->db->query("SHOW COLUMNS FROM ".$busbarTable." LIKE 'phases_config'");
if (!$resql2 || $this->db->num_rows($resql2) == 0) {
$this->db->query("ALTER TABLE ".$busbarTable." ADD COLUMN phases_config TEXT DEFAULT NULL AFTER phases");
}
}
}
/**
* Migration v11.1.0: Räumlichkeit (output_location) für Abgänge
*/
private function migrate_v1110_output_location()
{
$table = MAIN_DB_PREFIX."kundenkarte_equipment_connection";
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
if (!$resql || $this->db->num_rows($resql) == 0) {
return;
}
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'output_location'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN output_location varchar(255) DEFAULT NULL AFTER output_label");
}
}
/**
* Function called when module is disabled.
* Remove from database constants, boxes and permissions from Dolibarr database.

View file

@ -2119,6 +2119,13 @@ body.kundenkarte-drag-active * {
margin-top: 20px !important;
max-width: 100% !important;
overflow-x: auto !important;
display: flex !important;
flex-direction: column !important;
}
/* Prevent gap between header and message */
.schematic-editor-wrapper > * {
flex-shrink: 0 !important;
}
/* Prevent Schematic Editor from breaking Dolibarr layout */
@ -2135,6 +2142,37 @@ body.kundenkarte-drag-active * {
background: #252525 !important;
border: 1px solid #333 !important;
border-radius: 4px 4px 0 0 !important;
margin: 0 !important;
box-sizing: border-box !important;
min-height: 50px !important;
flex-wrap: wrap !important;
}
.schematic-editor-actions {
display: flex !important;
flex-wrap: wrap !important;
gap: 5px !important;
align-items: center !important;
}
/* Einheitliche Größe für alle Toolbar-Buttons (button + a) */
.schematic-editor-actions > button,
.schematic-editor-actions > a {
padding: 5px 8px !important;
background: #333 !important;
border: 1px solid #555 !important;
border-radius: 3px !important;
cursor: pointer !important;
font-size: 12px !important;
line-height: 1.4 !important;
height: 30px !important;
box-sizing: border-box !important;
display: inline-flex !important;
align-items: center !important;
gap: 4px !important;
text-decoration: none !important;
white-space: nowrap !important;
font-family: inherit !important;
}
.schematic-editor-toggle {
@ -2163,6 +2201,7 @@ body.kundenkarte-drag-active * {
min-height: 300px !important;
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
.schematic-editor-canvas.expanded {
@ -2214,13 +2253,19 @@ body.kundenkarte-drag-active * {
transition: filter 0.2s ease !important;
}
/* Terminals */
/* Terminals - hitarea captures all events */
.schematic-terminal {
pointer-events: none !important;
}
.schematic-terminal-hitarea {
cursor: crosshair !important;
pointer-events: all !important;
}
.schematic-terminal-circle {
transition: all 0.2s ease !important;
pointer-events: none !important;
}
.schematic-terminal:hover .schematic-terminal-circle {
@ -2260,7 +2305,7 @@ body.kundenkarte-drag-active * {
/* Messages - Fixed height status bar */
.schematic-message {
padding: 6px 15px !important;
margin-bottom: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
font-size: 12px !important;
height: 28px !important;
@ -2275,6 +2320,7 @@ body.kundenkarte-drag-active * {
border: 1px solid #3498db !important;
border-top: none !important;
border-bottom: none !important;
display: block !important;
}
.schematic-message.info {
@ -2846,3 +2892,43 @@ body.kundenkarte-drag-active * {
.edit-product-clear:hover {
color: #e74c3c !important;
}
/* ========================================
AUSGEBAUTE ELEMENTE (Decommissioned)
======================================== */
/* Ausgebaute Elemente im Baum standardmäßig ausgeblendet */
/* Funktion 1: kundenkarte-tree-node, Funktion 2: kundenkarte-tree-row */
.kundenkarte-tree .decommissioned {
display: none !important;
}
/* Sichtbar wenn Toggle aktiv */
.kundenkarte-tree.show-decommissioned .decommissioned {
display: flex !important;
opacity: 0.4 !important;
}
/* Ausgegraut: Label durchgestrichen */
.kundenkarte-tree.show-decommissioned .decommissioned .tree-label,
.kundenkarte-tree.show-decommissioned .decommissioned .tree-item-label {
text-decoration: line-through !important;
}
/* Badge "Ausgebaut" mit Datum */
.badge-decommissioned {
display: inline-block !important;
margin-left: 8px !important;
padding: 1px 6px !important;
font-size: 10px !important;
background: #8b4513 !important;
color: #ddd !important;
border-radius: 3px !important;
vertical-align: middle !important;
}
/* Toggle-Button aktiver Zustand */
#btn-toggle-decommissioned.active {
background: rgba(139, 69, 19, 0.3) !important;
border-color: #8b4513 !important;
}

40
css/pwa.css Normal file → Executable file
View file

@ -905,12 +905,14 @@ body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
cursor: pointer;
padding: 3px 2px;
padding: 3px 0;
border-radius: 4px;
transition: background 0.15s;
min-width: 0;
text-align: center;
}
/* Zeile 2 (obere Terminals): direkt am Equipment (margin-bottom negativ) */
@ -950,6 +952,8 @@ body {
width: 0;
height: 0;
flex-shrink: 0;
align-self: center;
margin: 0 auto;
}
.terminal-arrow-down {
@ -977,7 +981,7 @@ body {
.terminal-label-cell {
display: flex;
justify-content: center;
min-height: 20px;
min-height: 30px;
}
/* Obere Labels (Zeile 1): am unteren Rand ausrichten (zum Terminal hin) */
@ -1002,7 +1006,7 @@ body {
font-weight: 600;
color: #fff;
line-height: 1.1;
max-height: 80px;
max-height: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -1029,6 +1033,15 @@ body {
background: rgba(173, 140, 79, 0.4);
}
.terminal-label .output-location {
display: block;
font-style: italic;
font-weight: normal;
font-size: 8px;
color: rgba(255,255,255,0.5);
margin-top: 1px;
}
.terminal-label .cable-info {
font-weight: normal;
font-size: 7px;
@ -1144,6 +1157,21 @@ body {
stroke-linejoin: round;
}
/* Wire Toggle Button */
#btn-toggle-wires {
opacity: 0.5;
transition: opacity 0.2s, background 0.2s;
}
#btn-toggle-wires.active {
opacity: 1;
background: rgba(173, 140, 79, 0.3);
}
#btn-toggle-wires svg {
fill: currentColor;
}
/* Grid-Rows: 1=Labels oben, 2=Terminals oben, 3=Equipment, 4=Terminals unten, 5=Labels unten */
/* Add Button in Carrier (letzte Spalte, Zeile 2) */
.btn-add-equipment {
@ -2067,3 +2095,9 @@ body {
border: 2px dashed rgba(255, 255, 255, 0.25);
}
/* Leere TE-Positionen ohne Terminal (z.B. NEO 4TE aber nur 3 Terminals) */
.terminal-point.no-terminal {
visibility: hidden;
pointer-events: none;
}

0
img/pwa-icon-192.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
img/pwa-icon-512.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because it is too large Load diff

View file

@ -338,6 +338,11 @@
n.data.display_label = lines.join('\n');
}
// Ausgebaut-Markierung
if (n.data.decommissioned) {
n.classes = (n.classes || '') + ' decommissioned';
}
cyElements.push(n);
});
}
@ -539,6 +544,14 @@
'opacity': 0.6
}
},
// Ausgebaute Elemente - ausgegraut
{
selector: '.decommissioned',
style: {
'opacity': 0.35,
'border-style': 'dashed'
}
},
// Hover
{
selector: 'node:active',
@ -606,6 +619,35 @@
case 'add-child':
window.location.href = baseUrl + '&action=create&parent_id=' + anlageId;
break;
case 'toggle-decommissioned':
var nodeEl = self.cy.$('#n_' + anlageId);
var isDecomm = nodeEl.length && nodeEl.data('decommissioned');
if (isDecomm) {
// Wieder einbauen - Bestätigung
window.KundenKarte.showConfirm('Wieder einbauen', 'Element wieder als eingebaut markieren?', function() {
$.post(self.moduleUrl + '/ajax/anlage.php', {
action: 'toggle_decommissioned',
anlage_id: anlageId,
token: $('input[name="token"]').val() || ''
}, function(res) {
if (res.success) {
nodeEl.data('decommissioned', 0);
nodeEl.removeClass('decommissioned');
} else {
alert(res.error || 'Fehler');
}
}, 'json');
});
} else {
// Ausbauen - Dialog mit Datum
window.KundenKarte.showDecommissionDialog(anlageId, function(res) {
if (nodeEl.length) {
nodeEl.data('decommissioned', 1);
nodeEl.addClass('decommissioned');
}
});
}
break;
}
self.hideContextMenu();
});
@ -620,6 +662,15 @@
if (!this.contextMenuEl) return;
this.contextMenuNodeId = node.data('id').replace('n_', '');
// Ausgebaut-Label im Kontextmenü anpassen
var isDecomm = node.data('decommissioned');
var $decommLabel = $(this.contextMenuEl).find('.ctx-decommission-label');
var $decommIcon = $(this.contextMenuEl).find('.ctx-decommission i');
if ($decommLabel.length) {
$decommLabel.text(isDecomm ? 'Wieder einbauen' : 'Ausbauen');
$decommIcon.attr('class', isDecomm ? 'fa fa-plug' : 'fa fa-power-off');
}
var container = document.getElementById(this.containerId);
var rect = container.getBoundingClientRect();
var x = rect.left + renderedPos.x;

574
js/pwa.js Normal file → Executable file
View file

@ -36,6 +36,9 @@
offlineQueue: [],
isOnline: navigator.onLine,
// Display settings
showConnectionLines: false, // Leitungen standardmäßig ausgeblendet
// Current modal state
currentCarrierId: null,
editCarrierId: null, // null = Add-Modus, ID = Edit-Modus (Hutschiene)
@ -56,10 +59,14 @@
// ============================================
function init() {
// Register Service Worker
// Register Service Worker + Update erzwingen
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
.then(reg => console.log('[PWA] Service Worker registered'))
navigator.serviceWorker.register('sw.js', { updateViaCache: 'none' })
.then(reg => {
console.log('[PWA] Service Worker registered');
// Sofort nach Updates suchen
reg.update();
})
.catch(err => console.error('[PWA] SW registration failed:', err));
}
@ -186,6 +193,7 @@
// Editor actions
$('#btn-add-panel').on('click', () => openModal('add-panel'));
$('#btn-save-panel').on('click', handleSavePanel);
$('#btn-toggle-wires').on('click', handleToggleWires);
$('#editor-content').on('click', '.btn-add-carrier', handleAddCarrier);
$('#editor-content').on('click', '.carrier-header', handleCarrierClick);
@ -222,6 +230,17 @@
// Medium-Type Change -> Spezifikationen laden
$('#conn-medium-type').on('change', handleMediumTypeChange);
// Connection-Type Change -> Auto-Farbe bei Input-Phasen
$('#conn-type').on('change', function() {
if (App.connectionDirection === 'input') {
const phase = $(this).val();
if (phase) {
const color = getPhaseColor(phase);
$('#conn-color').val(color);
}
}
});
// Bestätigungsdialog
$('#btn-confirm-ok').on('click', function() {
closeModal('confirm');
@ -734,6 +753,7 @@
App.outputs = response.outputs || [];
App.inputs = response.inputs || [];
App.connections = response.connections || [];
App.busbars = response.busbars || [];
App.fieldMeta = response.field_meta || {};
// Cache for offline
@ -745,6 +765,7 @@
outputs: App.outputs,
inputs: App.inputs,
connections: App.connections,
busbars: App.busbars,
fieldMeta: App.fieldMeta
}));
@ -765,6 +786,7 @@
App.outputs = data.outputs || [];
App.inputs = data.inputs || [];
App.connections = data.connections || [];
App.busbars = data.busbars || [];
App.fieldMeta = data.fieldMeta || {};
renderEditor();
showToast('Offline - Zeige gecachte Daten', 'warning');
@ -812,6 +834,9 @@
return;
}
// Terminal-Farbpropagierung aufbauen (Phasenfarben an allen Terminals)
buildTerminalPhaseMap();
let html = '';
App.panels.forEach(panel => {
@ -864,21 +889,26 @@
html += `<span class="terminal-label-cell label-row-top bundled-label" style="${gridColStyle}" data-connection-id="${bundledTop.id}" data-equipment-id="${eq.id}" data-direction="output">`;
if (bundledTop.output_label) {
html += `<span class="terminal-label">${escapeHtml(bundledTop.output_label)}`;
if (bundledTop.output_location) html += `<span class="output-location">${escapeHtml(bundledTop.output_location)}</span>`;
if (cableInfo) html += `<span class="cable-info">${escapeHtml(cableInfo)}</span>`;
html += `</span>`;
}
html += `</span>`;
} else {
// Normale einzelne Labels pro Terminal - nur für tatsächliche Terminals
// Normale einzelne Labels pro Terminal - per Terminal-ID matchen
const eqTerminals = getTerminals(eq);
const topTerms = eqTerminals.filter(tm => tm.pos === 'top');
for (let t = 0; t < topTerminalCount; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:1;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const topOut = eqTopOutputs[t] || null;
const termId = topTerms[t] ? topTerms[t].id : ('t' + (t + 1));
const topOut = eqTopOutputs.find(o => o.source_terminal_id === termId) || eqTopOutputs[t] || null;
if (topOut && topOut.output_label && (!topOut.bundled_terminals || widthTe <= 1)) {
const cableInfo = buildCableInfo(topOut);
html += `<span class="terminal-label-cell label-row-top" style="${style}" data-connection-id="${topOut.id}" data-equipment-id="${eq.id}" data-direction="output">`;
html += `<span class="terminal-label">${escapeHtml(topOut.output_label)}`;
if (topOut.output_location) html += `<span class="output-location">${escapeHtml(topOut.output_location)}</span>`;
if (cableInfo) html += `<span class="cable-info">${escapeHtml(cableInfo)}</span>`;
html += `</span>`;
html += `</span>`;
@ -899,38 +929,43 @@
carrierEquipment.forEach(eq => {
const widthTe = parseFloat(eq.width_te) || 1;
const posTe = parseFloat(eq.position_te) || 0;
const eqInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id && i.target_terminal_id === 't1') : [];
const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : [];
// Terminal-Anzahl aus terminals_config ermitteln
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
const topTerminalCount = getTerminalCount(type, 'top', widthTe);
// Terminal-IDs für diese Position ermitteln
const eqTerminals = getTerminals(eq);
const topTerms = eqTerminals.filter(tm => tm.pos === 'top');
const topTermIds = topTerms.map(tm => tm.id);
// Inputs und Outputs per Terminal-ID matchen (nicht per Index!)
const eqTopInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id && topTermIds.indexOf(i.target_terminal_id) !== -1) : [];
const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : [];
// Gebündelter Abgang?
const bundledTop = eqTopOutputs.find(o => o.bundled_terminals === 'all');
// Nur so viele Terminal-Punkte wie tatsächlich konfiguriert
for (let t = 0; t < topTerminalCount; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:2;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const inp = eqInputs[t] || null;
const topOut = bundledTop || eqTopOutputs[t] || null;
const termId = topTerms[t] ? topTerms[t].id : ('t' + (t + 1));
// Input/Output per Terminal-ID finden
const inp = eqTopInputs.find(i => i.target_terminal_id === termId) || null;
const topOut = bundledTop || eqTopOutputs.find(o => o.source_terminal_id === termId) || eqTopOutputs[t] || null;
if (bundledTop && widthTe > 1) {
// Gebündelter Abgang: Pfeil nur beim ersten Terminal, Rest leer
if (t === 0) {
const phaseColor = bundledTop.color || getPhaseColor(bundledTop.connection_type);
const bundledStyle = posTe > 0
? `grid-row:2; grid-column: ${posTe} / span ${topTerminalCount}`
: `grid-row:2; grid-column: span ${topTerminalCount}`;
? `grid-row:2; grid-column: ${posTe} / span ${widthTe}`
: `grid-row:2; grid-column: span ${widthTe}`;
html += `<span class="terminal-point terminal-output terminal-row-top bundled-output" data-equipment-id="${eq.id}" data-direction="output" data-terminal-position="top" data-connection-id="${bundledTop.id}" style="${bundledStyle}">`;
html += `<span class="terminal-arrow terminal-arrow-up" style="--arrow-color:${phaseColor}"></span>`;
html += `<span class="terminal-phase">${escapeHtml(bundledTop.connection_type || '')}</span>`;
html += `</span>`;
}
// Restliche Terminals überspringen (grid-column: span hat sie schon)
} else if (topOut && (!topOut.bundled_terminals || widthTe <= 1)) {
// Normaler Top-Output ODER bundled bei 1 TE (Bundle macht bei 1 TE keinen Unterschied)
} else if (topOut && topOut.output_label && (!topOut.bundled_terminals || widthTe <= 1)) {
// Output MIT Label → Pfeil (echter Abgang)
const phaseColor = topOut.color || getPhaseColor(topOut.connection_type);
html += `<span class="terminal-point terminal-output terminal-row-top" data-equipment-id="${eq.id}" data-direction="output" data-terminal-position="top" data-connection-id="${topOut.id}" style="${style}">`;
html += `<span class="terminal-arrow terminal-arrow-up" style="--arrow-color:${phaseColor}"></span>`;
@ -943,13 +978,22 @@
html += `<span class="terminal-phase">${escapeHtml(inp.connection_type || '')}</span>`;
html += `</span>`;
} else {
// Leerer Terminal - neutral, Position "top"
html += `<span class="terminal-point terminal-empty terminal-row-top" data-equipment-id="${eq.id}" data-terminal-position="top" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot terminal-dot-empty"></span>`;
html += `</span>`;
// Phasenfarbe aus Propagierung
const propColor = (App.terminalColorMap[eq.id] || {})[termId];
const propPhase = (App.terminalPhaseMap[eq.id] || {})[termId];
if (propColor) {
html += `<span class="terminal-point terminal-propagated terminal-row-top" data-equipment-id="${eq.id}" data-terminal-position="top" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot" style="background:${propColor}"></span>`;
html += `<span class="terminal-phase">${escapeHtml(propPhase || '')}</span>`;
html += `</span>`;
} else {
html += `<span class="terminal-point terminal-empty terminal-row-top" data-equipment-id="${eq.id}" data-terminal-position="top" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot terminal-dot-empty"></span>`;
html += `</span>`;
}
}
}
// Leere Zellen für restliche TE-Breite (ohne Terminal-Punkte)
// Leere Zellen für restliche TE-Breite
for (let t = topTerminalCount; t < widthTe; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:2;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
@ -1018,38 +1062,43 @@
carrierEquipment.forEach(eq => {
const widthTe = parseFloat(eq.width_te) || 1;
const posTe = parseFloat(eq.position_te) || 0;
const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : [];
const eqBottomInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id && i.target_terminal_id === 't2') : [];
// Terminal-Anzahl aus terminals_config ermitteln
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
const bottomTerminalCount = getTerminalCount(type, 'bottom', widthTe);
// Terminal-IDs für Bottom ermitteln
const eqTerminals = getTerminals(eq);
const botTerms = eqTerminals.filter(tm => tm.pos === 'bottom');
const botTermIds = botTerms.map(tm => tm.id);
// Inputs und Outputs per Terminal-ID matchen
const eqBottomInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id && botTermIds.indexOf(i.target_terminal_id) !== -1) : [];
const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : [];
// Gebündelter Abgang?
const bundledBottom = eqBottomOutputs.find(o => o.bundled_terminals === 'all');
// Nur so viele Terminal-Punkte wie tatsächlich konfiguriert
for (let t = 0; t < bottomTerminalCount; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:4;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const out = bundledBottom || eqBottomOutputs[t] || null;
const inp = eqBottomInputs[t] || null;
const termId = botTerms[t] ? botTerms[t].id : ('t' + (widthTe + t + 1));
// Input/Output per Terminal-ID finden
const out = bundledBottom || eqBottomOutputs.find(o => o.source_terminal_id === termId) || eqBottomOutputs[t] || null;
const inp = eqBottomInputs.find(i => i.target_terminal_id === termId) || null;
if (bundledBottom && widthTe > 1) {
// Gebündelter Abgang: Pfeil nur beim ersten Terminal, Rest leer
if (t === 0) {
const phaseColor = bundledBottom.color || getPhaseColor(bundledBottom.connection_type);
const bundledStyle = posTe > 0
? `grid-row:4; grid-column: ${posTe} / span ${bottomTerminalCount}`
: `grid-row:4; grid-column: span ${bottomTerminalCount}`;
? `grid-row:4; grid-column: ${posTe} / span ${widthTe}`
: `grid-row:4; grid-column: span ${widthTe}`;
html += `<span class="terminal-point terminal-output terminal-row-bottom bundled-output" data-equipment-id="${eq.id}" data-direction="output" data-terminal-position="bottom" data-connection-id="${bundledBottom.id}" style="${bundledStyle}">`;
html += `<span class="terminal-arrow terminal-arrow-down" style="--arrow-color:${phaseColor}"></span>`;
html += `<span class="terminal-phase">${escapeHtml(bundledBottom.connection_type || '')}</span>`;
html += `</span>`;
}
// Restliche Terminals überspringen (grid-column: span hat sie schon)
} else if (out && (!out.bundled_terminals || widthTe <= 1)) {
// Normaler Abgang ODER bundled bei 1 TE (Bundle macht bei 1 TE keinen Unterschied)
} else if (out && out.output_label && (!out.bundled_terminals || widthTe <= 1)) {
// Output MIT Label → Pfeil (echter Abgang)
const phaseColor = out.color || getPhaseColor(out.connection_type);
html += `<span class="terminal-point terminal-output terminal-row-bottom" data-equipment-id="${eq.id}" data-direction="output" data-terminal-position="bottom" data-connection-id="${out.id}" style="${style}">`;
html += `<span class="terminal-arrow terminal-arrow-down" style="--arrow-color:${phaseColor}"></span>`;
@ -1062,13 +1111,22 @@
html += `<span class="terminal-phase">${escapeHtml(inp.connection_type || '')}</span>`;
html += `</span>`;
} else {
// Leerer Terminal - neutral, Position "bottom"
html += `<span class="terminal-point terminal-empty terminal-row-bottom" data-equipment-id="${eq.id}" data-terminal-position="bottom" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot terminal-dot-empty"></span>`;
html += `</span>`;
// Phasenfarbe aus Propagierung
const propColor = (App.terminalColorMap[eq.id] || {})[termId];
const propPhase = (App.terminalPhaseMap[eq.id] || {})[termId];
if (propColor) {
html += `<span class="terminal-point terminal-propagated terminal-row-bottom" data-equipment-id="${eq.id}" data-terminal-position="bottom" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot" style="background:${propColor}"></span>`;
html += `<span class="terminal-phase">${escapeHtml(propPhase || '')}</span>`;
html += `</span>`;
} else {
html += `<span class="terminal-point terminal-empty terminal-row-bottom" data-equipment-id="${eq.id}" data-terminal-position="bottom" data-connection-id="" style="${style}">`;
html += `<span class="terminal-dot terminal-dot-empty"></span>`;
html += `</span>`;
}
}
}
// Leere Zellen für restliche TE-Breite (ohne Terminal-Punkte)
// Leere Zellen für restliche TE-Breite
for (let t = bottomTerminalCount; t < widthTe; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:4;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
@ -1098,21 +1156,26 @@
html += `<span class="terminal-label-cell label-row-bottom bundled-label" style="${gridColStyle}" data-connection-id="${bundledBottom.id}" data-equipment-id="${eq.id}" data-direction="output">`;
if (bundledBottom.output_label) {
html += `<span class="terminal-label">${escapeHtml(bundledBottom.output_label)}`;
if (bundledBottom.output_location) html += `<span class="output-location">${escapeHtml(bundledBottom.output_location)}</span>`;
if (cableInfo) html += `<span class="cable-info">${escapeHtml(cableInfo)}</span>`;
html += `</span>`;
}
html += `</span>`;
} else {
// Normale einzelne Labels pro Terminal - nur für tatsächliche Terminals
// Normale einzelne Labels pro Terminal - per Terminal-ID matchen
const eqTerminals = getTerminals(eq);
const botTerms = eqTerminals.filter(tm => tm.pos === 'bottom');
for (let t = 0; t < bottomTerminalCount; t++) {
const colPos = posTe > 0 ? posTe + t : 0;
const style = `grid-row:5;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
const out = eqBottomOutputs[t] || null;
const termId = botTerms[t] ? botTerms[t].id : ('t' + (widthTe + t + 1));
const out = eqBottomOutputs.find(o => o.source_terminal_id === termId) || eqBottomOutputs[t] || null;
if (out && out.output_label && (!out.bundled_terminals || widthTe <= 1)) {
const cableInfo = buildCableInfo(out);
html += `<span class="terminal-label-cell label-row-bottom" style="${style}" data-connection-id="${out.id}" data-equipment-id="${eq.id}" data-direction="output">`;
html += `<span class="terminal-label">${escapeHtml(out.output_label)}`;
if (out.output_location) html += `<span class="output-location">${escapeHtml(out.output_location)}</span>`;
if (cableInfo) html += `<span class="cable-info">${escapeHtml(cableInfo)}</span>`;
html += `</span>`;
html += `</span>`;
@ -1152,14 +1215,25 @@
}
/**
* Render SVG connection lines from path_data
* Only shows connections that were manually drawn on the website
* Render SVG connection lines between equipment
* PWA uses different layout than desktop, so we calculate positions dynamically
*/
function renderConnectionLines() {
// Remove existing SVG overlays first
$('.connection-lines-svg').remove();
// Only render if setting is enabled
if (!App.showConnectionLines) {
return;
}
if (!App.connections || App.connections.length === 0) {
return;
}
// Desktop reference dimensions
const DESKTOP_TE_WIDTH = 56;
// Für jede Hutschiene ein SVG-Overlay erstellen
$('.carrier-card').each(function() {
const $carrier = $(this);
@ -1172,6 +1246,18 @@
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId);
const equipmentIds = carrierEquipment.map(e => e.id);
// Carrier-Daten für Total-TE
const carrier = App.carriers.find(c => c.id == carrierId);
const totalTe = carrier ? (parseInt(carrier.total_te) || 12) : 12;
// PWA TE-Breite berechnen
const carrierWidth = $content.width();
const pwaTeWidth = carrierWidth / (totalTe + 1); // +1 für den Add-Button
// Scale factor: PWA-Breite / Desktop-Breite
const scaleX = pwaTeWidth / DESKTOP_TE_WIDTH;
const scaleY = scaleX * 0.8; // Y etwas weniger skalieren (PWA ist kompakter)
// Verbindungen filtern die zu dieser Hutschiene gehören
const carrierConnections = App.connections.filter(c =>
equipmentIds.includes(parseInt(c.fk_source)) ||
@ -1195,16 +1281,88 @@
const color = conn.color || getPhaseColor(conn.connection_type);
// Transform path_data coordinates to PWA scale
const scaledPath = transformPathData(conn.path_data, scaleX, scaleY);
// Schatten-Pfad für bessere Sichtbarkeit
svgContent += `<path class="connection-shadow" d="${conn.path_data}" />`;
svgContent += `<path class="connection-shadow" d="${scaledPath}" />`;
// Hauptpfad
svgContent += `<path class="connection-line" d="${conn.path_data}" style="stroke:${color}" data-connection-id="${conn.id}" />`;
svgContent += `<path class="connection-line" d="${scaledPath}" style="stroke:${color}" data-connection-id="${conn.id}" />`;
// Label falls vorhanden
if (conn.output_label) {
const labelPos = getPathMidpoint(scaledPath);
if (labelPos) {
const labelWidth = Math.min(conn.output_label.length * 6 + 10, 80);
svgContent += `<rect x="${labelPos.x - labelWidth/2}" y="${labelPos.y - 8}" width="${labelWidth}" height="16" rx="3" fill="#1a1a1a" stroke="${color}" stroke-width="1"/>`;
svgContent += `<text x="${labelPos.x}" y="${labelPos.y + 4}" text-anchor="middle" fill="${color}" font-size="10" font-weight="bold">${escapeHtml(conn.output_label)}</text>`;
}
}
});
$svg.html(svgContent);
});
}
/**
* Transform path data coordinates by scale factors
*/
function transformPathData(pathData, scaleX, scaleY) {
if (!pathData) return '';
// Parse and transform coordinates
return pathData.replace(/([ML])\s*([\d.-]+)\s+([\d.-]+)/gi, function(match, cmd, x, y) {
const newX = (parseFloat(x) * scaleX).toFixed(1);
const newY = (parseFloat(y) * scaleY).toFixed(1);
return `${cmd} ${newX} ${newY}`;
});
}
/**
* Get midpoint of a path for label positioning
*/
function getPathMidpoint(pathData) {
if (!pathData) return null;
const points = [];
const regex = /[ML]\s*([\d.-]+)\s+([\d.-]+)/gi;
let match;
while ((match = regex.exec(pathData)) !== null) {
points.push({ x: parseFloat(match[1]), y: parseFloat(match[2]) });
}
if (points.length < 2) return null;
// Calculate midpoint along path
let totalLength = 0;
const segments = [];
for (let i = 1; i < points.length; i++) {
const dx = points[i].x - points[i-1].x;
const dy = points[i].y - points[i-1].y;
const len = Math.sqrt(dx*dx + dy*dy);
segments.push({ start: points[i-1], end: points[i], length: len });
totalLength += len;
}
const halfLength = totalLength / 2;
let accumulated = 0;
for (const seg of segments) {
if (accumulated + seg.length >= halfLength) {
const t = (halfLength - accumulated) / seg.length;
return {
x: seg.start.x + t * (seg.end.x - seg.start.x),
y: seg.start.y + t * (seg.end.y - seg.start.y)
};
}
accumulated += seg.length;
}
return { x: (points[0].x + points[points.length-1].x) / 2, y: (points[0].y + points[points.length-1].y) / 2 };
}
function renderTypeGrid() {
const categoryLabels = {
'automat': 'Leitungsschutz',
@ -1238,6 +1396,27 @@
$('#type-grid').html(html);
}
// ============================================
// WIRE DISPLAY TOGGLE
// ============================================
function handleToggleWires() {
App.showConnectionLines = !App.showConnectionLines;
// Update button appearance
const $btn = $('#btn-toggle-wires');
if (App.showConnectionLines) {
$btn.addClass('active');
$btn.attr('title', 'Leitungen ausblenden');
} else {
$btn.removeClass('active');
$btn.attr('title', 'Leitungen einblenden');
}
// Re-render connection lines
renderConnectionLines();
}
// ============================================
// PANEL (FELD) ACTIONS
// ============================================
@ -1319,7 +1498,13 @@
}
const totalTe = parseInt(teBtn.data('te'));
const label = $('#carrier-label').val().trim() || 'Hutschiene';
const panelId = App.editCarrierId
? (App.carriers.find(c => c.id == App.editCarrierId)?.fk_panel || App.currentPanelId)
: App.currentPanelId;
const panelCarrierCount = App.carriers.filter(c => c.fk_panel == panelId).length;
const inputLabel = $('#carrier-label').val().trim();
// CREATE: +1 weil neuer Carrier noch nicht in der Liste; UPDATE: inkl. sich selbst gezählt
const label = inputLabel || (App.editCarrierId ? 'R' + panelCarrierCount : 'R' + (panelCarrierCount + 1));
closeModal('add-carrier');
@ -1614,7 +1799,9 @@
html += `<option value="">--</option>`;
if (field.options) {
field.options.split('|').forEach(opt => {
const selected = (opt === val) ? ' selected' : '';
opt = opt.trim();
if (!opt) return;
const selected = (opt === val.trim()) ? ' selected' : '';
html += `<option value="${escapeHtml(opt)}"${selected}>${escapeHtml(opt)}</option>`;
});
}
@ -2233,6 +2420,7 @@
$('#btn-delete-connection').removeClass('hidden');
$('#conn-color').val(conn.color || '#3498db');
$('#conn-label').val(conn.output_label || '');
$('#conn-location').val(conn.output_location || '');
$('#conn-medium-length').val(conn.medium_length || '');
// Medium-Typen laden und Select befüllen
@ -2255,7 +2443,8 @@
// Side-Buttons immer zeigen
$('#conn-side-fields').show();
// Medium-Felder nur bei Abgang zeigen
// Räumlichkeit und Medium-Felder nur bei Abgang zeigen
$('#conn-location-fields').toggle(direction === 'output');
$('#conn-output-fields').toggle(direction === 'output');
// Bundle-Option: Nur bei Abgang + Equipment mit mehr als 1 Terminal
@ -2274,7 +2463,7 @@
}
// Phasen-Optionen wie auf der Website
const INPUT_PHASES = ['L1', 'L2', 'L3', '3P', '3P+N', 'PE'];
const INPUT_PHASES = ['L1', 'L2', 'L3', 'N', 'PE'];
const OUTPUT_PHASES = ['LN', 'N', '3P+N', 'PE', 'DATA'];
/**
@ -2337,6 +2526,200 @@
return colors[idx];
}
/**
* Terminals eines Equipment ermitteln (aus terminals_config oder Fallback)
* @param {object} eq - Equipment-Objekt
* @returns {Array} [{id: 't1', pos: 'top'}, ...]
*/
function getTerminals(eq) {
const type = App.equipmentTypes ? App.equipmentTypes.find(t => t.id == eq.fk_equipment_type) : null;
if (type && type.terminals_config) {
try {
const configStr = typeof type.terminals_config === 'string'
? type.terminals_config.replace(/\\r\\n|\\r|\\n/g, ' ')
: '';
const config = typeof type.terminals_config === 'string'
? JSON.parse(configStr)
: type.terminals_config;
if (config.terminals && Array.isArray(config.terminals)) {
return config.terminals;
}
} catch (e) { /* Parse-Fehler ignorieren */ }
}
// Fallback: Fortlaufende t1, t2, t3... IDs (gleiche Konvention wie Website)
// Standard-LS: t1 (top), t2 (bottom)
// Breiteres Equipment: t1..tN (top), t(N+1)..t(2N) (bottom)
const widthTe = parseFloat(eq.width_te) || 1;
const terminals = [];
for (let i = 0; i < widthTe; i++) {
terminals.push({id: 't' + (i + 1), pos: 'top', col: i});
}
for (let i = 0; i < widthTe; i++) {
terminals.push({id: 't' + (widthTe + i + 1), pos: 'bottom', col: i});
}
return terminals;
}
/**
* Phasen-Labels aus Kürzel parsen (z.B. "3P" ["L1","L2","L3"])
*/
function parsePhaseLabels(phases) {
if (!phases) return [];
const p = phases.toUpperCase();
if (p === '3P' || p === 'L1L2L3') return ['L1', 'L2', 'L3'];
if (p === '3P+N' || p === '3PN') return ['L1', 'L2', 'L3', 'N'];
if (p === '3P+N+PE' || p === '3PNPE') return ['L1', 'L2', 'L3', 'N', 'PE'];
if (p === 'L1N' || p === 'L1+N') return ['L1', 'N'];
if (p === 'L1') return ['L1'];
if (p === 'L2') return ['L2'];
if (p === 'L3') return ['L3'];
if (p === 'N') return ['N'];
if (p === 'PE') return ['PE'];
if (p.indexOf('+') !== -1) return p.split('+');
if (p.indexOf(',') !== -1) return p.split(',');
return [phases];
}
/**
* Terminal-Farbpropagierung aufbauen
* Setzt Phasen direkt anhand der Verbindungsdaten KEINE Block-Durchreichung.
*
* Ablauf:
* 1. Inputs setzen Phase auf ihr Ziel-Terminal
* 2. Outputs setzen Phase auf ihr Quell-Terminal
* 3. Wires setzen Phase auf BEIDE Enden (connection_type der Leitung)
* 4. Busbars verteilen Phasen an überlappende Equipment-Terminals
*/
function buildTerminalPhaseMap() {
const phaseMap = {}; // {eqId: {termId: "L1"}}
const colorMap = {}; // {eqId: {termId: "#hex"}}
function setPhase(eqId, termId, phase, color) {
if (!phaseMap[eqId]) phaseMap[eqId] = {};
if (!colorMap[eqId]) colorMap[eqId] = {};
if (phaseMap[eqId][termId]) return false; // Bereits gesetzt
phaseMap[eqId][termId] = phase;
colorMap[eqId][termId] = color || getPhaseColor(phase);
return true;
}
function forcePhase(eqId, termId, phase, color) {
if (!phaseMap[eqId]) phaseMap[eqId] = {};
if (!colorMap[eqId]) colorMap[eqId] = {};
if (phaseMap[eqId][termId] === phase) return false;
phaseMap[eqId][termId] = phase;
colorMap[eqId][termId] = color || getPhaseColor(phase);
return true;
}
// Schritt 1: Inputs setzen Phase auf Ziel-Terminal
if (App.inputs) {
App.inputs.forEach(function(inp) {
if (!inp.fk_target || !inp.target_terminal_id) return;
var phase = (inp.connection_type || '').toUpperCase();
if (!phase) return;
setPhase(inp.fk_target, inp.target_terminal_id, phase, inp.color || getPhaseColor(phase));
});
}
// Schritt 2: Outputs setzen Phase auf Quell-Terminal
if (App.outputs) {
App.outputs.forEach(function(out) {
if (!out.fk_source || !out.source_terminal_id) return;
var phase = (out.connection_type || '').toUpperCase();
if (!phase) return;
setPhase(out.fk_source, out.source_terminal_id, phase, out.color || getPhaseColor(phase));
});
}
// Schritt 3: Wires setzen Phase auf BEIDE Enden
// connection_type der Leitung bestimmt die Phase direkt
if (App.connections) {
App.connections.forEach(function(conn) {
if (!conn.fk_source || !conn.fk_target) return;
var phase = (conn.connection_type || '').toUpperCase();
if (!phase) return;
var wireColor = conn.color || getPhaseColor(phase);
if (conn.source_terminal_id) {
setPhase(conn.fk_source, conn.source_terminal_id, phase, wireColor);
}
if (conn.target_terminal_id) {
setPhase(conn.fk_target, conn.target_terminal_id, phase, wireColor);
}
});
}
// Schritt 4: Busbars verteilen Phasen an überlappende Equipment-Terminals
if (App.busbars && App.equipment) {
App.busbars.forEach(function(busbar) {
var railStart = busbar.rail_start_te || 1;
var railEnd = busbar.rail_end_te || railStart;
var targetPos = (busbar.position_y === 0) ? 'top' : 'bottom';
// Phase-Labels ermitteln
var phaseLabels;
if (busbar.phases_config && Array.isArray(busbar.phases_config) && busbar.phases_config.length > 0) {
phaseLabels = busbar.phases_config;
} else {
phaseLabels = parsePhaseLabels(busbar.rail_phases || busbar.connection_type || '');
}
if (phaseLabels.length === 0) return;
// Prüfen ob mindestens eine Phase eingespeist wird
var anyPhaseFed = false;
App.equipment.forEach(function(eq) {
if (String(eq.fk_carrier) !== String(busbar.fk_carrier)) return;
var eqPos = parseFloat(eq.position_te) || 1;
var eqWidth = parseFloat(eq.width_te) || 1;
if (!(eqPos < railEnd + 1 && railStart < eqPos + eqWidth)) return;
var terms = getTerminals(eq);
terms.filter(function(t) { return t.pos === targetPos; }).forEach(function(term) {
if ((phaseMap[eq.id] || {})[term.id]) {
anyPhaseFed = true;
}
});
});
// Nur verteilen wenn mindestens eine Phase anliegt
if (!anyPhaseFed) return;
// Excluded TEs
var excludedTEs = busbar.excluded_te
? busbar.excluded_te.split(',').map(function(t) { return parseInt(t.trim()); }).filter(function(t) { return !isNaN(t); })
: [];
// Phasen auf Equipment verteilen
App.equipment.forEach(function(eq) {
if (String(eq.fk_carrier) !== String(busbar.fk_carrier)) return;
var eqPos = parseFloat(eq.position_te) || 1;
var eqWidth = parseFloat(eq.width_te) || 1;
if (!(eqPos < railEnd + 1 && railStart < eqPos + eqWidth)) return;
var terms = getTerminals(eq);
var posTerminals = terms.filter(function(t) { return t.pos === targetPos; });
posTerminals.forEach(function(term, idx) {
var teIndex = term.col !== undefined ? term.col : (idx % eqWidth);
var absoluteTE = Math.round(eqPos + teIndex);
if (excludedTEs.indexOf(absoluteTE) !== -1) return;
if (absoluteTE < railStart || absoluteTE > railEnd) return;
var teOffset = absoluteTE - railStart;
var phase = phaseLabels[teOffset % phaseLabels.length];
var phaseColor = getPhaseColor(phase);
forcePhase(eq.id, term.id, phase, phaseColor);
});
});
});
}
// Als App-State speichern
App.terminalPhaseMap = phaseMap;
App.terminalColorMap = colorMap;
}
/**
* Abgangsseite-Button setzen
*/
@ -2452,7 +2835,7 @@
<div class="terminal-context-menu" style="position:fixed;left:${x}px;top:${y}px;z-index:10001;">
<div class="tcm-item tcm-input" data-type="input">
<span class="tcm-icon" style="color:#f39c12;"></span>
<span>Anschlusspunkt (L1/L2/L3)</span>
<span>Anschlusspunkt (Einspeisung)</span>
</div>
<div class="tcm-item tcm-output" data-type="output">
<span class="tcm-icon" style="color:#3498db;"></span>
@ -2576,6 +2959,7 @@
$('#btn-delete-connection').addClass('hidden');
$('#conn-color').val('#3498db');
$('#conn-label').val('');
$('#conn-location').val('');
$('#conn-medium-length').val('');
// Medium-Typen laden und Select befüllen
@ -2588,7 +2972,8 @@
// Side-Buttons immer zeigen (Automaten haben keine feste Richtung)
$('#conn-side-fields').show();
// Medium-Felder nur bei Abgang zeigen
// Räumlichkeit und Medium-Felder nur bei Abgang zeigen
$('#conn-location-fields').toggle(direction === 'output');
$('#conn-output-fields').toggle(direction === 'output');
// Bundle-Option: Nur bei Abgang + Equipment mit mehr als 1 Terminal
@ -2612,6 +2997,7 @@
const connectionType = $('#conn-type').val() || '';
const color = $('#conn-color').val() || '#3498db';
const outputLabel = $('#conn-label').val().trim();
const outputLocation = $('#conn-location').val().trim();
const isOutput = App.connectionDirection === 'output';
const mediumType = isOutput ? ($('#conn-medium-type').val().trim() || '') : '';
const mediumSpec = isOutput ? ($('#conn-medium-spec').val().trim() || '') : '';
@ -2640,6 +3026,7 @@
connection_type: connectionType,
color: color,
output_label: outputLabel,
output_location: outputLocation,
medium_type: mediumType,
medium_spec: mediumSpec,
medium_length: mediumLength,
@ -2653,6 +3040,7 @@
conn.connection_type = connectionType;
conn.color = color;
conn.output_label = outputLabel;
conn.output_location = outputLocation;
conn.medium_type = mediumType;
conn.medium_spec = mediumSpec;
conn.medium_length = mediumLength;
@ -2668,7 +3056,13 @@
const response = await apiCall('ajax/pwa_api.php', data);
if (response.success) {
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
updateLocal(list.find(c => c.id == App.editConnectionId));
const conn = list.find(c => c.id == App.editConnectionId);
updateLocal(conn);
// Farbpropagierung bei Input
if (App.connectionDirection === 'input' && connectionType) {
const eqId = conn ? conn.fk_target : null;
await propagateInputColor(eqId, connectionType, color);
}
renderEditor();
showToast('Verbindung aktualisiert', 'success');
} else {
@ -2681,7 +3075,13 @@
} else {
queueOfflineAction(data);
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
updateLocal(list.find(c => c.id == App.editConnectionId));
const conn = list.find(c => c.id == App.editConnectionId);
updateLocal(conn);
// Farbpropagierung bei Input (offline)
if (App.connectionDirection === 'input' && connectionType) {
const eqId = conn ? conn.fk_target : null;
propagateInputColor(eqId, connectionType, color);
}
renderEditor();
showToast('Wird synchronisiert...', 'warning');
}
@ -2694,6 +3094,7 @@
connection_type: connectionType,
color: color,
output_label: outputLabel,
output_location: outputLocation,
medium_type: mediumType,
medium_spec: mediumSpec,
medium_length: mediumLength,
@ -2707,6 +3108,7 @@
connection_type: connectionType,
color: color,
output_label: outputLabel,
output_location: outputLocation,
medium_type: mediumType,
medium_spec: mediumSpec,
medium_length: mediumLength,
@ -2724,6 +3126,10 @@
if (App.connectionDirection === 'input') {
newConn.fk_target = App.connectionEquipmentId;
App.inputs.push(newConn);
// Farbpropagierung bei neuem Input
if (connectionType) {
await propagateInputColor(App.connectionEquipmentId, connectionType, color);
}
} else {
newConn.fk_source = App.connectionEquipmentId;
App.outputs.push(newConn);
@ -2743,6 +3149,10 @@
if (App.connectionDirection === 'input') {
newConn.fk_target = App.connectionEquipmentId;
App.inputs.push(newConn);
// Farbpropagierung bei neuem Input (offline)
if (connectionType) {
propagateInputColor(App.connectionEquipmentId, connectionType, color);
}
} else {
newConn.fk_source = App.connectionEquipmentId;
App.outputs.push(newConn);
@ -2755,6 +3165,60 @@
App.editConnectionId = null;
}
/**
* Farbpropagierung: Wenn Input-Phase gesetzt, Farbe auf Outputs übertragen
* @param {number} equipmentId - Equipment-ID
* @param {string} phase - Phase (L1, L2, L3, N, PE)
* @param {string} color - Farbe der Einspeisung
*/
async function propagateInputColor(equipmentId, phase, color) {
if (!equipmentId || !phase || !color) return;
// Finde alle Outputs dieses Equipment
const outputs = App.outputs.filter(o => o.fk_source == equipmentId);
if (outputs.length === 0) return;
// Update Outputs mit passender Phase
for (const output of outputs) {
// Phase-Matching: L1 -> L1, LN, L1N; L2 -> L2; etc.
const outputType = output.connection_type || '';
let matches = false;
if (phase === 'L1' && (outputType.includes('L1') || outputType === 'LN')) matches = true;
else if (phase === 'L2' && outputType.includes('L2')) matches = true;
else if (phase === 'L3' && outputType.includes('L3')) matches = true;
else if (phase === 'N' && outputType.includes('N')) matches = true;
else if (phase === 'PE' && outputType.includes('PE')) matches = true;
if (matches && output.color !== color) {
output.color = color;
// Backend aktualisieren
if (App.isOnline) {
try {
await apiCall('ajax/pwa_api.php', {
action: 'update_connection',
connection_id: output.id,
color: color
});
} catch (err) {
queueOfflineAction({
action: 'update_connection',
connection_id: output.id,
color: color
});
}
} else {
queueOfflineAction({
action: 'update_connection',
connection_id: output.id,
color: color
});
}
}
}
}
/**
* Connection löschen (mit Bestätigung)
*/

View file

@ -555,3 +555,28 @@ ViewModes = Verfuegbare Ansichten
ViewModesBoth = Baum & Graph
ViewModesTreeOnly = Nur Baum
ViewModesGraphOnly = Nur Graph
# Ausgebaut-Status
Decommissioned = Ausgebaut
Decommission = Ausbauen
Recommission = Wieder einbauen
ShowDecommissioned = Ausgebaute Elemente anzeigen
ShowDecommissionedDefault = Ausgebaute Elemente standardmäßig anzeigen
# Eigener Betrieb & Zubehör
CompanyTools = Mein Betrieb
HasAccessories = Hat Zubehör
HasAccessoriesHelp = Ermöglicht die Zuordnung von Zubehör und Ersatzteilen
HasProduct = Produkt-Zuordnung
HasProductHelp = Ermöglicht die Verknüpfung mit einem Dolibarr-Produkt
Accessories = Zubehör / Ersatzteile
NoAccessories = Kein Zubehör zugeordnet
SearchProduct = Produkt suchen
SelectSupplier = Lieferant auswählen
CreateSupplierOrder = Lieferantenbestellung erstellen
OrderAccessories = Zubehör bestellen
OrderGeneratedFromAccessories = Bestellung aus Anlagen-Zubehör generiert
ConfirmDeleteAccessory = Dieses Zubehör wirklich entfernen?
NoToolsYet = Noch keine Werkzeuge erfasst
AddFirstTool = Erstes Werkzeug hinzufügen
GoToTypeAdmin = Zur Typverwaltung

View file

@ -303,3 +303,28 @@ ViewModes = Available Views
ViewModesBoth = Tree & Graph
ViewModesTreeOnly = Tree Only
ViewModesGraphOnly = Graph Only
# Decommissioned status
Decommissioned = Decommissioned
Decommission = Decommission
Recommission = Recommission
ShowDecommissioned = Show decommissioned elements
ShowDecommissionedDefault = Show decommissioned elements by default
# Own Company & Accessories
CompanyTools = My Company
HasAccessories = Has Accessories
HasAccessoriesHelp = Allows assigning accessories and spare parts
HasProduct = Product Assignment
HasProductHelp = Allows linking to a Dolibarr product
Accessories = Accessories / Spare Parts
NoAccessories = No accessories assigned
SearchProduct = Search product
SelectSupplier = Select supplier
CreateSupplierOrder = Create supplier order
OrderAccessories = Order accessories
OrderGeneratedFromAccessories = Order generated from installation accessories
ConfirmDeleteAccessory = Really remove this accessory?
NoToolsYet = No tools registered yet
AddFirstTool = Add first tool
GoToTypeAdmin = Go to type administration

View file

@ -151,6 +151,7 @@ function kundenkarte_graph_print_container($params)
print '<a class="ctx-item ctx-add-child" data-action="add-child"><i class="fa fa-plus"></i> '.$langs->trans('AddChild').'</a>';
print '<a class="ctx-item ctx-edit" data-action="edit"><i class="fa fa-edit"></i> '.$langs->trans('Edit').'</a>';
print '<a class="ctx-item ctx-copy" data-action="copy"><i class="fa fa-copy"></i> '.$langs->trans('Copy').'</a>';
print '<a class="ctx-item ctx-decommission" data-action="toggle-decommissioned"><i class="fa fa-power-off"></i> <span class="ctx-decommission-label">'.$langs->trans('Decommission').'</span></a>';
}
if ($permissiontodelete) {
print '<a class="ctx-item ctx-delete" data-action="delete"><i class="fa fa-trash"></i> '.$langs->trans('Delete').'</a>';

2131
lib/wiring_diagram.lib.php Normal file

File diff suppressed because it is too large Load diff

0
manifest.json Normal file → Executable file
View file

40
pwa.php Normal file → Executable file
View file

@ -44,8 +44,37 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="img/pwa-icon-192.png">
<link rel="apple-touch-icon" href="img/pwa-icon-192.png">
<link rel="stylesheet" href="css/pwa.css?v=5.3">
<link rel="stylesheet" href="css/pwa.css?v=5.9">
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
<script>
// Einmaliger Cache-Reset (v12.4) — löscht alte Service Worker + Caches + Editor-Daten
(function() {
var REQUIRED_VERSION = 'v12.4';
if (localStorage.getItem('sw_version') !== REQUIRED_VERSION) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(regs) {
regs.forEach(function(r) { r.unregister(); });
});
}
if ('caches' in window) {
caches.keys().then(function(names) {
names.forEach(function(n) { caches.delete(n); });
});
}
// Gecachte Editor-Daten löschen (kundenkarte_data_*)
var keysToRemove = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.indexOf('kundenkarte_data_') === 0) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(function(k) { localStorage.removeItem(k); });
localStorage.setItem('sw_version', REQUIRED_VERSION);
setTimeout(function() { location.reload(true); }, 500);
}
})();
</script>
</head>
<body>
<div id="app" class="app">
@ -133,6 +162,9 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
<svg viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
<span id="sync-badge" class="sync-badge hidden">0</span>
</button>
<button id="btn-toggle-wires" class="btn-icon" title="Leitungen ein-/ausblenden">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" class="wire-icon-off"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17l-4-4 1.4-1.4 2.6 2.6 6.6-6.6L17 9l-8 8z" class="wire-icon-on hidden"/></svg>
</button>
</header>
<div id="editor-content" class="editor-content">
@ -243,6 +275,10 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
<label>Bezeichnung</label>
<input type="text" id="conn-label" class="form-input" placeholder="z.B. Küche Steckdosen">
</div>
<div id="conn-location-fields" class="form-group">
<label>Räumlichkeit</label>
<input type="text" id="conn-location" class="form-input" placeholder="z.B. Küche, Bad OG, Keller">
</div>
<!-- Anschlussseite: Immer sichtbar (Automaten haben keine feste Richtung) -->
<div id="conn-side-fields" class="form-group">
<label>Anschlussseite</label>
@ -374,6 +410,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
</script>
<script src="js/pwa.js?v=5.1"></script>
<script src="js/pwa.js?v=5.9"></script>
</body>
</html>

0
pwa_auth.php Normal file → Executable file
View file

View file

@ -0,0 +1,12 @@
-- ========================================================================
-- Copyright (C) 2024 Data IT Solution
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- ========================================================================
ALTER TABLE llx_kundenkarte_anlage_accessory ADD UNIQUE INDEX uk_anlage_accessory (fk_anlage, fk_product);
ALTER TABLE llx_kundenkarte_anlage_accessory ADD INDEX idx_accessory_anlage (fk_anlage);
ALTER TABLE llx_kundenkarte_anlage_accessory ADD CONSTRAINT fk_accessory_anlage FOREIGN KEY (fk_anlage) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE;

View file

@ -0,0 +1,19 @@
-- ========================================================================
-- Copyright (C) 2024 Data IT Solution
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- ========================================================================
CREATE TABLE llx_kundenkarte_anlage_accessory (
rowid integer AUTO_INCREMENT PRIMARY KEY,
fk_anlage integer NOT NULL,
fk_product integer NOT NULL,
qty double DEFAULT 1,
rang integer DEFAULT 0,
note varchar(255) DEFAULT NULL,
date_creation datetime DEFAULT NULL,
fk_user_creat integer DEFAULT NULL
) ENGINE=InnoDB;

7
sw.js Normal file → Executable file
View file

@ -3,8 +3,8 @@
* Offline-First für Schaltschrank-Dokumentation
*/
const CACHE_NAME = 'kundenkarte-pwa-v6.1';
const OFFLINE_CACHE = 'kundenkarte-offline-v6.1';
const CACHE_NAME = 'kundenkarte-pwa-v12.5';
const OFFLINE_CACHE = 'kundenkarte-offline-v12.4';
// Statische Assets die immer gecached werden (ohne Query-String)
const STATIC_ASSETS = [
@ -50,6 +50,9 @@ self.addEventListener('activate', event => {
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Nur http/https cachen - chrome-extension:// etc. überspringen
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
// PWA Auth Endpoints - immer Netzwerk
if (url.pathname.includes('pwa_auth.php')) {
event.respondWith(fetch(event.request));

View file

@ -29,6 +29,7 @@ dol_include_once('/kundenkarte/class/anlagefile.class.php');
dol_include_once('/kundenkarte/class/equipmentpanel.class.php');
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
dol_include_once('/kundenkarte/class/equipment.class.php');
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Load translation files
@ -177,6 +178,7 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
@ -219,6 +221,7 @@ if ($action == 'update' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type - but keep current system for GLOBAL types (buildings)
@ -507,6 +510,10 @@ if ($isTreeView) {
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
$showDecomm = getDolGlobalInt('KUNDENKARTE_SHOW_DECOMMISSIONED', 0);
print '<button type="button" class="button small'.($showDecomm ? ' active' : '').'" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
print '<i class="fa '.($showDecomm ? 'fa-eye' : 'fa-eye-slash').'"></i> <span>'.$langs->trans('Decommissioned').'</span>';
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
@ -587,13 +594,28 @@ if (empty($customerSystems)) {
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
// Zugeordnetes Produkt (nur wenn Typ es erlaubt)
if ($type->has_product && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
print '<tr><td>'.$langs->trans('Product').'</td>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$product->id.'">'.dol_escape_htmltag($product->ref).'</a>';
print ' - '.dol_escape_htmltag($product->label);
if ($product->price > 0) {
print ' <span class="opacitymedium">('.price($product->price).' €)</span>';
}
print '</td></tr>';
}
}
// Dynamic fields - all fields come from type definition
$fieldValues = $anlage->getFieldValues();
$typeFieldsList = $type->fetchFields();
foreach ($typeFieldsList as $field) {
if ($field->field_type === 'header') {
// Section header
print '<tr class="liste_titre"><th colspan="2" style="background:#f0f0f0;padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
print '<tr class="liste_titre"><th colspan="2" style="padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
} else {
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
if ($value !== '') {
@ -627,6 +649,16 @@ if (empty($customerSystems)) {
print '<td>'.dol_print_date($anlage->tms, 'dayhour').'</td></tr>';
}
// Ausgebaut-Status
if (!empty($anlage->decommissioned)) {
print '<tr><td>'.$langs->trans('Decommissioned').'</td>';
print '<td><span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$langs->trans('Decommissioned').'</span>';
if (!empty($anlage->date_decommissioned)) {
print ' '.dol_print_date(strtotime($anlage->date_decommissioned), 'day');
}
print '</td></tr>';
}
print '</table>';
// Files section
@ -749,6 +781,9 @@ if (empty($customerSystems)) {
print '<button type="button" class="schematic-add-busbar" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#f39c12;cursor:pointer;" title="Phasenschiene hinzufügen">';
print '<i class="fa fa-arrows-h"></i> Phasenschiene';
print '</button>';
print '<button type="button" class="schematic-straighten-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#2ecc71;cursor:pointer;" title="Diagonale Leitungen begradigen (nur rechte Winkel)">';
print '<i class="fa fa-align-justify"></i> Begradigen';
print '</button>';
print '<button type="button" class="schematic-clear-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#e74c3c;cursor:pointer;">';
print '<i class="fa fa-trash"></i> Alle Verbindungen löschen';
print '</button>';
@ -760,11 +795,20 @@ if (empty($customerSystems)) {
print '<button type="button" class="schematic-audit-log" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#95a5a6;cursor:pointer;" title="Änderungsprotokoll anzeigen">';
print '<i class="fa fa-history"></i> Protokoll';
print '</button>';
// Display Settings button
print '<button type="button" class="schematic-settings-btn" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;" title="Anzeigeeinstellungen (Leitungsfarben, -stärken, Terminal-Stil)">';
print '<i class="fa fa-cog"></i> Anzeige';
print '</button>';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '<a href="'.$pdfExportUrl.'" target="_blank" class="schematic-export-pdf" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="PDF Export (Leitungslaufplan nach DIN EN 61082)">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
// Leitungslaufplan PDF-Export (separates Feature)
$wiringUrl = dol_buildpath('/kundenkarte/ajax/export_wiring_diagram_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A3&orientation=L';
print '<a href="'.$wiringUrl.'" target="_blank" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#27ae60;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="Leitungslaufplan (DIN EN 61082)">';
print '<i class="fa fa-sitemap"></i> Leitungslaufplan';
print '</a>';
print '</div>';
print '</div>';
print '<div class="schematic-message info">Bereit</div>';
@ -784,6 +828,75 @@ if (empty($customerSystems)) {
</script>';
}
// Zubehör-Bereich (nur wenn Typ has_accessories hat)
if (!empty($type->has_accessories)) {
$accessoryObj = new AnlageAccessory($db);
$accessories = $accessoryObj->fetchAllByAnlage($anlageId);
print '<br><h4><i class="fa fa-puzzle-piece"></i> '.$langs->trans('Accessories').'</h4>';
if (!empty($accessories)) {
print '<div class="div-table-responsive">';
print '<table class="tagtable liste">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('ProductRef').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th class="right">'.$langs->trans('Qty').'</th>';
print '<th>'.$langs->trans('Note').'</th>';
if ($permissiontodelete) {
print '<th class="right">'.$langs->trans('Action').'</th>';
}
print '</tr>';
foreach ($accessories as $acc) {
print '<tr>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$acc->fk_product.'">'.dol_escape_htmltag($acc->product_ref).'</a></td>';
print '<td>'.dol_escape_htmltag($acc->product_label).'</td>';
print '<td class="right">'.$acc->qty.'</td>';
print '<td>'.dol_escape_htmltag($acc->note).'</td>';
if ($permissiontodelete) {
print '<td class="right"><a href="#" class="btn-delete-accessory" data-id="'.$acc->id.'" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a></td>';
}
print '</tr>';
}
print '</table>';
print '</div>';
} else {
print '<p class="opacitymedium">'.$langs->trans('NoAccessories').'</p>';
}
// Zubehör hinzufügen
if ($permissiontoadd) {
print '<div id="add-accessory-form" style="margin-top:10px;">';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<input type="text" id="accessory_product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'...">';
print '<input type="hidden" id="accessory_product_id" value="">';
print '<input type="number" id="accessory_qty" class="flat" value="1" min="1" style="width:80px;">';
print '<button type="button" id="btn-add-accessory" class="button small" disabled>'.$langs->trans('Add').'</button>';
print '</div>';
print '</div>';
}
// Bestellfunktion
if ($permissiontoadd && !empty($accessories)) {
print '<div style="margin-top:15px;padding:10px;border:1px solid #444;border-radius:4px;">';
print '<h5 style="margin:0 0 10px 0;"><i class="fa fa-shopping-cart"></i> '.$langs->trans('OrderAccessories').'</h5>';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<select id="supplier_select" class="flat minwidth200">';
print '<option value="">'.$langs->trans('SelectSupplier').'</option>';
$sqlSupp = "SELECT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s WHERE s.fournisseur = 1 AND s.status = 1 ORDER BY s.nom";
$resSupp = $db->query($sqlSupp);
if ($resSupp) {
while ($objSupp = $db->fetch_object($resSupp)) {
print '<option value="'.$objSupp->rowid.'">'.dol_escape_htmltag($objSupp->nom).'</option>';
}
}
print '</select>';
print '<button type="button" id="btn-order-accessories" class="button small">'.$langs->trans('CreateSupplierOrder').'</button>';
print '</div>';
print '</div>';
}
}
// Action buttons
print '<div class="tabsAction">';
if ($permissiontoadd) {
@ -883,7 +996,7 @@ if (empty($customerSystems)) {
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
$picto = !empty($t->picto) ? dol_escape_htmltag($t->picto) : '';
$color = !empty($t->color) ? dol_escape_htmltag($t->color) : '';
print '<option value="'.$t->id.'" data-category="building" data-icon="'.$picto.'" data-color="'.$color.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
print '<option value="'.$t->id.'" data-category="building" data-icon="'.$picto.'" data-color="'.$color.'" data-has-product="'.($t->has_product ? '1' : '0').'" data-has-accessories="'.($t->has_accessories ? '1' : '0').'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
if ($lastGroup !== '') print '</optgroup>';
}
@ -895,7 +1008,7 @@ if (empty($customerSystems)) {
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
$picto = !empty($t->picto) ? dol_escape_htmltag($t->picto) : '';
$color = !empty($t->color) ? dol_escape_htmltag($t->color) : '';
print '<option value="'.$t->id.'" data-category="element" data-icon="'.$picto.'" data-color="'.$color.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
print '<option value="'.$t->id.'" data-category="element" data-icon="'.$picto.'" data-color="'.$color.'" data-has-product="'.($t->has_product ? '1' : '0').'" data-has-accessories="'.($t->has_accessories ? '1' : '0').'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
print '</optgroup>';
}
@ -916,6 +1029,23 @@ if (empty($customerSystems)) {
printTreeOptions($tree, $selectedParent, $excludeId);
print '</select></td></tr>';
// Produkt-Zuordnung (wird per JS ein-/ausgeblendet je nach Typ)
$productValue = '';
$productId = 0;
if (($isEdit || $isCopy) && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
$productValue = $product->ref.' - '.$product->label;
$productId = $product->id;
}
}
print '<tr id="row_product" style="display:none;"><td>'.$langs->trans('Product').'</td>';
print '<td>';
print '<input type="text" id="product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'..." value="'.dol_escape_htmltag($productValue).'">';
print '<input type="hidden" name="fk_product" id="fk_product" value="'.$productId.'">';
print '</td></tr>';
// Dynamic fields will be inserted here via JavaScript
print '<tbody id="dynamic_fields"></tbody>';
@ -1005,15 +1135,15 @@ if (empty($customerSystems)) {
$typeSelect.prop("disabled", false);
$("#row_type").show();
// Wert wiederherstellen falls noch vorhanden
if (currentVal && $typeSelect.find("option[value=\"" + currentVal + "\"]").length) {
$typeSelect.val(currentVal);
} else {
$typeSelect.val("");
}
// Select2 neu initialisieren
initSelect2();
// Wert wiederherstellen falls noch vorhanden (nach Select2-Init mit trigger)
if (currentVal && $typeSelect.find("option[value=\"" + currentVal + "\"]").length) {
$typeSelect.val(currentVal).trigger("change");
} else {
$typeSelect.val("").trigger("change");
}
}
$catSelect.on("change", function() {
@ -1022,9 +1152,24 @@ if (empty($customerSystems)) {
$typeSelect.trigger("change");
});
// Produkt-Zeile ein-/ausblenden je nach Typ-Flag has_product
function updateProductRow() {
var $selected = $typeSelect.find("option:selected");
var hasProduct = $selected.data("has-product");
if (hasProduct == 1) {
$("#row_product").show();
} else {
$("#row_product").hide();
$("#fk_product").val("");
$("#product_search").val("");
}
}
$typeSelect.on("change", updateProductRow);
// Initial filtern
if ($catSelect.val()) {
filterTypes();
updateProductRow();
} else {
$typeSelect.prop("disabled", true);
$("#row_type").hide();
@ -1092,7 +1237,7 @@ if (empty($customerSystems)) {
$connectionsByTarget = array();
if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">';
print '<div class="kundenkarte-tree'.($showDecomm ? ' show-decommissioned' : '').'" data-system="'.$systemId.'" data-socid="'.$id.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
} else {
@ -1109,6 +1254,168 @@ print dol_get_fiche_end();
// Tooltip container
print '<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>';
// Produkt-Autocomplete + Zubehör-AJAX (nur wenn Formular oder Detailansicht aktiv)
if (in_array($action, array('create', 'edit', 'copy', 'view'))) {
print '<script>
$(document).ready(function() {
var baseUrl = "'.dol_escape_js(dol_buildpath('/kundenkarte', 1)).'";
// Produkt-Autocomplete
function initProductAutocomplete(inputSelector, hiddenSelector) {
var $input = $(inputSelector);
var $hidden = $(hiddenSelector);
if (!$input.length) return;
var searchTimeout;
$input.on("input", function() {
clearTimeout(searchTimeout);
var term = $(this).val();
if (term.length < 2) {
$hidden.val("");
$(".product-autocomplete-dropdown").remove();
return;
}
searchTimeout = setTimeout(function() {
$.get(baseUrl + "/ajax/equipment.php", {
action: "get_products",
term: term,
token: $("input[name=token]").val() || ""
}, function(data) {
$(".product-autocomplete-dropdown").remove();
if (data.success && data.products && data.products.length > 0) {
var $dropdown = $("<div class=\"product-autocomplete-dropdown\"></div>");
$.each(data.products, function(i, p) {
var label = p.ref + " - " + p.label;
if (p.price > 0) label += " (" + p.price + " \u20ac)";
$dropdown.append(
$("<div class=\"product-autocomplete-item\"></div>")
.text(label)
.data("id", p.id)
.data("ref", p.ref)
.data("label", p.label)
);
});
$input.after($dropdown);
$dropdown.on("click", ".product-autocomplete-item", function() {
$input.val($(this).data("ref") + " - " + $(this).data("label"));
$hidden.val($(this).data("id"));
$dropdown.remove();
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", false);
}
});
}
});
}, 300);
});
$(document).on("click", function(e) {
if (!$(e.target).closest(inputSelector + ", .product-autocomplete-dropdown").length) {
$(".product-autocomplete-dropdown").remove();
}
});
$input.on("change", function() {
if (!$(this).val()) {
$hidden.val("");
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", true);
}
}
});
}
initProductAutocomplete("#product_search", "#fk_product");
initProductAutocomplete("#accessory_product_search", "#accessory_product_id");
// Zubehör hinzufügen
$("#btn-add-accessory").on("click", function() {
var productId = $("#accessory_product_id").val();
var qty = $("#accessory_qty").val() || 1;
var anlageId = '.((int) $anlageId).';
if (!productId || !anlageId) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "add",
fk_anlage: anlageId,
fk_product: productId,
qty: qty,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) { location.reload(); }
else { alert(data.error || "Fehler"); }
});
});
// Zubehör löschen
$(".btn-delete-accessory").on("click", function(e) {
e.preventDefault();
if (!confirm("'.$langs->trans('ConfirmDeleteAccessory').'")) return;
var accId = $(this).data("id");
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "delete",
id: accId,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) { location.reload(); }
else { alert(data.error || "Fehler"); }
});
});
// Lieferantenbestellung
$("#btn-order-accessories").on("click", function() {
var supplierId = $("#supplier_select").val();
var anlageId = '.((int) $anlageId).';
if (!supplierId) { alert("Bitte Lieferant auswählen"); return; }
var ids = [];
$(".btn-delete-accessory").each(function() { ids.push($(this).data("id")); });
if (ids.length === 0) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "order",
fk_anlage: anlageId,
supplier_id: supplierId,
"ids[]": ids,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success && data.order_id) {
window.location.href = "'.DOL_URL_ROOT.'/fourn/commande/card.php?id=" + data.order_id;
} else {
alert(data.error || "Fehler beim Erstellen der Bestellung");
}
});
});
});
</script>';
// CSS für Autocomplete
print '<style>
.product-autocomplete-dropdown {
position: absolute;
z-index: 1000;
background: var(--colorbackbody, #fff);
border: 1px solid #555;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.product-autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #333;
}
.product-autocomplete-item:hover {
background: var(--colorbacklinepairhover, #333);
}
.product-autocomplete-item:last-child {
border-bottom: none;
}
</style>';
}
llxFooter();
$db->close();
@ -1227,6 +1534,9 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
if ($node->type_can_have_equipment) {
$nodeClass .= ' node-equipment'; // Geräte-Container (Schaltschrank, Verteiler)
} elseif ($node->type_can_have_children) {
@ -1262,6 +1572,14 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
}
print '</span>';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
@ -1304,6 +1622,9 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
@ -1473,6 +1794,9 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable';
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
print '<div class="'.$nodeClass.'">';
@ -1529,6 +1853,14 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
}
print '</span>';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
@ -1568,6 +1900,9 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';

View file

@ -29,6 +29,7 @@ dol_include_once('/kundenkarte/class/anlagefile.class.php');
dol_include_once('/kundenkarte/class/equipmentpanel.class.php');
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
dol_include_once('/kundenkarte/class/equipment.class.php');
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Load translation files
@ -178,6 +179,7 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
@ -220,6 +222,7 @@ if ($action == 'update' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type - but keep current system for GLOBAL types (buildings)
@ -505,6 +508,10 @@ if ($isTreeView) {
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
$showDecomm = getDolGlobalInt('KUNDENKARTE_SHOW_DECOMMISSIONED', 0);
print '<button type="button" class="button small'.($showDecomm ? ' active' : '').'" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
print '<i class="fa '.($showDecomm ? 'fa-eye' : 'fa-eye-slash').'"></i> <span>'.$langs->trans('Decommissioned').'</span>';
print '</button>';
if ($systemId > 0) {
$exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$object->socid.'&contactid='.$id.'&system='.$systemId;
print '<a class="button small" href="'.$exportUrl.'" title="'.$langs->trans('ExportPDF').'" target="_blank">';
@ -585,13 +592,28 @@ if (empty($customerSystems)) {
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
// Zugeordnetes Produkt (nur wenn Typ es erlaubt)
if ($type->has_product && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
print '<tr><td>'.$langs->trans('Product').'</td>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$product->id.'">'.dol_escape_htmltag($product->ref).'</a>';
print ' - '.dol_escape_htmltag($product->label);
if ($product->price > 0) {
print ' <span class="opacitymedium">('.price($product->price).' €)</span>';
}
print '</td></tr>';
}
}
// Dynamic fields - all fields come from type definition
$fieldValues = $anlage->getFieldValues();
$typeFieldsList = $type->fetchFields();
foreach ($typeFieldsList as $field) {
if ($field->field_type === 'header') {
// Section header
print '<tr class="liste_titre"><th colspan="2" style="background:#f0f0f0;padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
print '<tr class="liste_titre"><th colspan="2" style="padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
} else {
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
if ($value !== '') {
@ -625,6 +647,16 @@ if (empty($customerSystems)) {
print '<td>'.dol_print_date($anlage->tms, 'dayhour').'</td></tr>';
}
// Ausgebaut-Status
if (!empty($anlage->decommissioned)) {
print '<tr><td>'.$langs->trans('Decommissioned').'</td>';
print '<td><span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$langs->trans('Decommissioned').'</span>';
if (!empty($anlage->date_decommissioned)) {
print ' '.dol_print_date(strtotime($anlage->date_decommissioned), 'day');
}
print '</td></tr>';
}
print '</table>';
// Files section
@ -747,6 +779,9 @@ if (empty($customerSystems)) {
print '<button type="button" class="schematic-add-busbar" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#f39c12;cursor:pointer;" title="Phasenschiene hinzufügen">';
print '<i class="fa fa-arrows-h"></i> Phasenschiene';
print '</button>';
print '<button type="button" class="schematic-straighten-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#2ecc71;cursor:pointer;" title="Diagonale Leitungen begradigen (nur rechte Winkel)">';
print '<i class="fa fa-align-justify"></i> Begradigen';
print '</button>';
print '<button type="button" class="schematic-clear-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#e74c3c;cursor:pointer;">';
print '<i class="fa fa-trash"></i> Alle Verbindungen löschen';
print '</button>';
@ -758,11 +793,20 @@ if (empty($customerSystems)) {
print '<button type="button" class="schematic-audit-log" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#95a5a6;cursor:pointer;" title="Änderungsprotokoll anzeigen">';
print '<i class="fa fa-history"></i> Protokoll';
print '</button>';
// Display Settings button
print '<button type="button" class="schematic-settings-btn" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;" title="Anzeigeeinstellungen (Leitungsfarben, -stärken, Terminal-Stil)">';
print '<i class="fa fa-cog"></i> Anzeige';
print '</button>';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '<a href="'.$pdfExportUrl.'" target="_blank" class="schematic-export-pdf" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="PDF Export (Leitungslaufplan nach DIN EN 61082)">';
print '<i class="fa fa-file-pdf-o"></i> PDF Export';
print '</a>';
// Leitungslaufplan PDF-Export (separates Feature)
$wiringUrl = dol_buildpath('/kundenkarte/ajax/export_wiring_diagram_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A3&orientation=L';
print '<a href="'.$wiringUrl.'" target="_blank" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#27ae60;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="Leitungslaufplan (DIN EN 61082)">';
print '<i class="fa fa-sitemap"></i> Leitungslaufplan';
print '</a>';
print '</div>';
print '</div>';
print '<div class="schematic-message info">Bereit</div>';
@ -782,6 +826,73 @@ if (empty($customerSystems)) {
</script>';
}
// Zubehör-Bereich (nur wenn Typ has_accessories hat)
if (!empty($type->has_accessories)) {
$accessoryObj = new AnlageAccessory($db);
$accessories = $accessoryObj->fetchAllByAnlage($anlageId);
print '<br><h4><i class="fa fa-puzzle-piece"></i> '.$langs->trans('Accessories').'</h4>';
if (!empty($accessories)) {
print '<div class="div-table-responsive">';
print '<table class="tagtable liste">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('ProductRef').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th class="right">'.$langs->trans('Qty').'</th>';
print '<th>'.$langs->trans('Note').'</th>';
if ($permissiontodelete) {
print '<th class="right">'.$langs->trans('Action').'</th>';
}
print '</tr>';
foreach ($accessories as $acc) {
print '<tr>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$acc->fk_product.'">'.dol_escape_htmltag($acc->product_ref).'</a></td>';
print '<td>'.dol_escape_htmltag($acc->product_label).'</td>';
print '<td class="right">'.$acc->qty.'</td>';
print '<td>'.dol_escape_htmltag($acc->note).'</td>';
if ($permissiontodelete) {
print '<td class="right"><a href="#" class="btn-delete-accessory" data-id="'.$acc->id.'" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a></td>';
}
print '</tr>';
}
print '</table>';
print '</div>';
} else {
print '<p class="opacitymedium">'.$langs->trans('NoAccessories').'</p>';
}
if ($permissiontoadd) {
print '<div id="add-accessory-form" style="margin-top:10px;">';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<input type="text" id="accessory_product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'...">';
print '<input type="hidden" id="accessory_product_id" value="">';
print '<input type="number" id="accessory_qty" class="flat" value="1" min="1" style="width:80px;">';
print '<button type="button" id="btn-add-accessory" class="button small" disabled>'.$langs->trans('Add').'</button>';
print '</div>';
print '</div>';
}
if ($permissiontoadd && !empty($accessories)) {
print '<div style="margin-top:15px;padding:10px;border:1px solid #444;border-radius:4px;">';
print '<h5 style="margin:0 0 10px 0;"><i class="fa fa-shopping-cart"></i> '.$langs->trans('OrderAccessories').'</h5>';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<select id="supplier_select" class="flat minwidth200">';
print '<option value="">'.$langs->trans('SelectSupplier').'</option>';
$sqlSupp = "SELECT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s WHERE s.fournisseur = 1 AND s.status = 1 ORDER BY s.nom";
$resSupp = $db->query($sqlSupp);
if ($resSupp) {
while ($objSupp = $db->fetch_object($resSupp)) {
print '<option value="'.$objSupp->rowid.'">'.dol_escape_htmltag($objSupp->nom).'</option>';
}
}
print '</select>';
print '<button type="button" id="btn-order-accessories" class="button small">'.$langs->trans('CreateSupplierOrder').'</button>';
print '</div>';
print '</div>';
}
}
// Action buttons
print '<div class="tabsAction">';
if ($permissiontoadd) {
@ -881,7 +992,7 @@ if (empty($customerSystems)) {
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
$picto = !empty($t->picto) ? dol_escape_htmltag($t->picto) : '';
$color = !empty($t->color) ? dol_escape_htmltag($t->color) : '';
print '<option value="'.$t->id.'" data-category="building" data-icon="'.$picto.'" data-color="'.$color.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
print '<option value="'.$t->id.'" data-category="building" data-icon="'.$picto.'" data-color="'.$color.'" data-has-product="'.($t->has_product ? '1' : '0').'" data-has-accessories="'.($t->has_accessories ? '1' : '0').'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
if ($lastGroup !== '') print '</optgroup>';
}
@ -893,7 +1004,7 @@ if (empty($customerSystems)) {
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
$picto = !empty($t->picto) ? dol_escape_htmltag($t->picto) : '';
$color = !empty($t->color) ? dol_escape_htmltag($t->color) : '';
print '<option value="'.$t->id.'" data-category="element" data-icon="'.$picto.'" data-color="'.$color.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
print '<option value="'.$t->id.'" data-category="element" data-icon="'.$picto.'" data-color="'.$color.'" data-has-product="'.($t->has_product ? '1' : '0').'" data-has-accessories="'.($t->has_accessories ? '1' : '0').'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
print '</optgroup>';
}
@ -914,6 +1025,23 @@ if (empty($customerSystems)) {
printTreeOptions($tree, $selectedParent, $excludeId);
print '</select></td></tr>';
// Produkt-Zuordnung (wird per JS ein-/ausgeblendet je nach Typ)
$productValue = '';
$productId = 0;
if (($isEdit || $isCopy) && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
$productValue = $product->ref.' - '.$product->label;
$productId = $product->id;
}
}
print '<tr id="row_product" style="display:none;"><td>'.$langs->trans('Product').'</td>';
print '<td>';
print '<input type="text" id="product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'..." value="'.dol_escape_htmltag($productValue).'">';
print '<input type="hidden" name="fk_product" id="fk_product" value="'.$productId.'">';
print '</td></tr>';
// Dynamic fields will be inserted here via JavaScript
print '<tbody id="dynamic_fields"></tbody>';
@ -1003,15 +1131,15 @@ if (empty($customerSystems)) {
$typeSelect.prop("disabled", false);
$("#row_type").show();
// Wert wiederherstellen falls noch vorhanden
if (currentVal && $typeSelect.find("option[value=\"" + currentVal + "\"]").length) {
$typeSelect.val(currentVal);
} else {
$typeSelect.val("");
}
// Select2 neu initialisieren
initSelect2();
// Wert wiederherstellen falls noch vorhanden (nach Select2-Init mit trigger)
if (currentVal && $typeSelect.find("option[value=\"" + currentVal + "\"]").length) {
$typeSelect.val(currentVal).trigger("change");
} else {
$typeSelect.val("").trigger("change");
}
}
$catSelect.on("change", function() {
@ -1020,9 +1148,24 @@ if (empty($customerSystems)) {
$typeSelect.trigger("change");
});
// Produkt-Zeile ein-/ausblenden je nach Typ-Flag has_product
function updateProductRow() {
var $selected = $typeSelect.find("option:selected");
var hasProduct = $selected.data("has-product");
if (hasProduct == 1) {
$("#row_product").show();
} else {
$("#row_product").hide();
$("#fk_product").val("");
$("#product_search").val("");
}
}
$typeSelect.on("change", updateProductRow);
// Initial filtern
if ($catSelect.val()) {
filterTypes();
updateProductRow();
} else {
$typeSelect.prop("disabled", true);
$("#row_type").hide();
@ -1090,7 +1233,7 @@ if (empty($customerSystems)) {
$connectionsByTarget = array();
if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
print '<div class="kundenkarte-tree'.($showDecomm ? ' show-decommissioned' : '').'" data-system="'.$systemId.'" data-socid="'.$object->socid.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
} else {
@ -1107,6 +1250,163 @@ print dol_get_fiche_end();
// Tooltip container
print '<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>';
// Produkt-Autocomplete + Zubehör-AJAX (nur wenn Formular oder Detailansicht aktiv)
if (in_array($action, array('create', 'edit', 'copy', 'view'))) {
print '<script>
$(document).ready(function() {
var baseUrl = "'.dol_escape_js(dol_buildpath('/kundenkarte', 1)).'";
function initProductAutocomplete(inputSelector, hiddenSelector) {
var $input = $(inputSelector);
var $hidden = $(hiddenSelector);
if (!$input.length) return;
var searchTimeout;
$input.on("input", function() {
clearTimeout(searchTimeout);
var term = $(this).val();
if (term.length < 2) {
$hidden.val("");
$(".product-autocomplete-dropdown").remove();
return;
}
searchTimeout = setTimeout(function() {
$.get(baseUrl + "/ajax/equipment.php", {
action: "get_products",
term: term,
token: $("input[name=token]").val() || ""
}, function(data) {
$(".product-autocomplete-dropdown").remove();
if (data.success && data.products && data.products.length > 0) {
var $dropdown = $("<div class=\"product-autocomplete-dropdown\"></div>");
$.each(data.products, function(i, p) {
var label = p.ref + " - " + p.label;
if (p.price > 0) label += " (" + p.price + " \u20ac)";
$dropdown.append(
$("<div class=\"product-autocomplete-item\"></div>")
.text(label)
.data("id", p.id)
.data("ref", p.ref)
.data("label", p.label)
);
});
$input.after($dropdown);
$dropdown.on("click", ".product-autocomplete-item", function() {
$input.val($(this).data("ref") + " - " + $(this).data("label"));
$hidden.val($(this).data("id"));
$dropdown.remove();
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", false);
}
});
}
});
}, 300);
});
$(document).on("click", function(e) {
if (!$(e.target).closest(inputSelector + ", .product-autocomplete-dropdown").length) {
$(".product-autocomplete-dropdown").remove();
}
});
$input.on("change", function() {
if (!$(this).val()) {
$hidden.val("");
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", true);
}
}
});
}
initProductAutocomplete("#product_search", "#fk_product");
initProductAutocomplete("#accessory_product_search", "#accessory_product_id");
$("#btn-add-accessory").on("click", function() {
var productId = $("#accessory_product_id").val();
var qty = $("#accessory_qty").val() || 1;
var anlageId = '.((int) $anlageId).';
if (!productId || !anlageId) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "add",
fk_anlage: anlageId,
fk_product: productId,
qty: qty,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) { location.reload(); }
else { alert(data.error || "Fehler"); }
});
});
$(".btn-delete-accessory").on("click", function(e) {
e.preventDefault();
if (!confirm("'.$langs->trans('ConfirmDeleteAccessory').'")) return;
var accId = $(this).data("id");
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "delete",
id: accId,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) { location.reload(); }
else { alert(data.error || "Fehler"); }
});
});
$("#btn-order-accessories").on("click", function() {
var supplierId = $("#supplier_select").val();
var anlageId = '.((int) $anlageId).';
if (!supplierId) { alert("Bitte Lieferant auswählen"); return; }
var ids = [];
$(".btn-delete-accessory").each(function() { ids.push($(this).data("id")); });
if (ids.length === 0) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "order",
fk_anlage: anlageId,
supplier_id: supplierId,
"ids[]": ids,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success && data.order_id) {
window.location.href = "'.DOL_URL_ROOT.'/fourn/commande/card.php?id=" + data.order_id;
} else {
alert(data.error || "Fehler beim Erstellen der Bestellung");
}
});
});
});
</script>';
print '<style>
.product-autocomplete-dropdown {
position: absolute;
z-index: 1000;
background: var(--colorbackbody, #fff);
border: 1px solid #555;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.product-autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #333;
}
.product-autocomplete-item:hover {
background: var(--colorbacklinepairhover, #333);
}
.product-autocomplete-item:last-child {
border-bottom: none;
}
</style>';
}
llxFooter();
$db->close();
@ -1225,6 +1525,9 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
if ($node->type_can_have_equipment) {
$nodeClass .= ' node-equipment'; // Geräte-Container (Schaltschrank, Verteiler)
} elseif ($node->type_can_have_children) {
@ -1260,6 +1563,14 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
}
print '</span>';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
@ -1302,6 +1613,9 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
@ -1503,6 +1817,9 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable';
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
print '<div class="'.$nodeClass.'">';
@ -1559,6 +1876,14 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
}
print '</span>';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Spacer to push badges to the right
print '<span class="kundenkarte-tree-spacer"></span>';
@ -1598,6 +1923,9 @@ function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDe
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';

982
werkzeuge.php Executable file
View file

@ -0,0 +1,982 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Mein Betrieb: Baumansicht für eigene Maschinen, Werkzeuge und Geräte
* Multi-System mit System-Tabs (wie Kunden-Anlagen)
*/
// Load Dolibarr environment
$res = 0;
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/anlagetype.class.php');
dol_include_once('/kundenkarte/class/anlagefile.class.php');
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Übersetzungen
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
// Berechtigungen
if (!$user->hasRight('kundenkarte', 'read')) {
accessforbidden();
}
$permissiontoread = $user->hasRight('kundenkarte', 'read');
$permissiontoadd = $user->hasRight('kundenkarte', 'write');
$permissiontodelete = $user->hasRight('kundenkarte', 'delete');
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$systemId = GETPOSTINT('system');
$anlageId = GETPOSTINT('anlage_id');
$parentId = GETPOSTINT('parent_id');
// Virtuelle Firma-ID für "Mein Betrieb" - braucht keinen echten Societe-Eintrag
// Die Anlage-Tabelle verwendet diese ID nur als Gruppierung
$socId = 99999999;
// ALLE verfügbaren Systeme laden
$allSystems = array();
$sql = "SELECT rowid, code, label, picto, color FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$allSystems[$obj->rowid] = $obj;
}
}
// Für diesen virtuellen Betrieb aktivierte Systeme laden
$customerSystems = array();
$sql = "SELECT ss.rowid, ss.fk_system, s.code, s.label, s.picto, s.color";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system ss";
$sql .= " JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system s ON s.rowid = ss.fk_system";
$sql .= " WHERE ss.fk_soc = ".((int) $socId)." AND (ss.fk_contact IS NULL OR ss.fk_contact = 0) AND ss.active = 1 AND s.active = 1";
$sql .= " AND s.code != 'GLOBAL'";
$sql .= " ORDER BY s.position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$customerSystems[$obj->fk_system] = $obj;
}
}
// Standard: Erstes aktiviertes System falls nicht angegeben
if (empty($systemId) && !empty($customerSystems)) {
$systemId = array_key_first($customerSystems);
}
// Objekte initialisieren
$form = new Form($db);
$anlage = new Anlage($db);
$anlageType = new AnlageType($db);
/*
* Actions
*/
// System hinzufügen
if ($action == 'add_system' && $permissiontoadd) {
$newSystemId = GETPOSTINT('new_system_id');
if ($newSystemId > 0 && !isset($customerSystems[$newSystemId])) {
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system";
$sql .= " (entity, fk_soc, fk_contact, fk_system, date_creation, fk_user_creat, active)";
$sql .= " VALUES (".$conf->entity.", ".((int) $socId).", 0, ".((int) $newSystemId).", NOW(), ".((int) $user->id).", 1)";
$result = $db->query($sql);
if ($result) {
setEventMessages($langs->trans('SystemAdded'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$newSystemId);
exit;
} else {
setEventMessages($db->lasterror(), null, 'errors');
}
}
$action = '';
}
// System entfernen
if ($action == 'confirm_remove_system' && $confirm == 'yes' && $permissiontodelete) {
$removeSystemId = GETPOSTINT('remove_system_id');
if ($removeSystemId > 0) {
// Prüfen ob System noch Elemente hat
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_soc = ".((int) $socId)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId);
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
if ($obj->cnt > 0) {
setEventMessages($langs->trans('ErrorSystemHasElements'), null, 'errors');
} else {
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system WHERE fk_soc = ".((int) $socId)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId);
$db->query($sql);
setEventMessages($langs->trans('SystemRemoved'), null, 'mesgs');
unset($customerSystems[$removeSystemId]);
if (!empty($customerSystems)) {
$systemId = array_key_first($customerSystems);
} else {
$systemId = 0;
}
}
}
header('Location: '.$_SERVER['PHP_SELF'].($systemId ? '?system='.$systemId : ''));
exit;
}
if ($action == 'add' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_soc = $socId;
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
// Dynamische Felder
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
if ($field->field_type === 'header') continue;
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
}
}
$anlage->setFieldValues($fieldValues);
}
$result = $anlage->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$systemId);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'create';
}
}
if ($action == 'update' && $permissiontoadd) {
$anlage->fetch($anlageId);
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Dynamische Felder
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
if ($field->field_type === 'header') continue;
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
}
}
$anlage->setFieldValues($fieldValues);
}
$result = $anlage->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$systemId);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'edit';
}
}
if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
$anlage->fetch($anlageId);
$result = $anlage->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
/*
* View
*/
$title = $langs->trans('CompanyTools');
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
print load_fiche_titre($title, '', 'fa-wrench');
print '<div class="fichecenter">';
// Bestätigungsdialoge
if ($action == 'delete') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?system='.$systemId.'&anlage_id='.$anlageId,
$langs->trans('DeleteElement'),
$langs->trans('ConfirmDeleteElement'),
'confirm_delete',
'',
'yes',
1
);
}
if ($action == 'remove_system') {
$removeSystemId = GETPOSTINT('remove_system_id');
$sysLabel = isset($customerSystems[$removeSystemId]) ? $customerSystems[$removeSystemId]->label : '';
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?remove_system_id='.$removeSystemId,
$langs->trans('RemoveSystem'),
$langs->trans('ConfirmRemoveSystem', $sysLabel),
'confirm_remove_system',
'',
'yes',
1
);
}
// System-Tabs
print '<div class="kundenkarte-system-tabs-wrapper">';
print '<div class="kundenkarte-system-tabs">';
foreach ($customerSystems as $sysId => $sys) {
$activeClass = ($sysId == $systemId) ? ' active' : '';
print '<div class="kundenkarte-system-tab'.$activeClass.'" data-system="'.$sysId.'">';
print '<a href="'.$_SERVER['PHP_SELF'].'?system='.$sysId.'" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:8px;">';
if ($sys->picto) {
print '<span class="kundenkarte-system-tab-icon" style="color:'.$sys->color.';">'.kundenkarte_render_icon($sys->picto).'</span>';
}
print '<span>'.dol_escape_htmltag($sys->label).'</span>';
print '</a>';
// Entfernen-Button (nur beim aktiven Tab)
if ($permissiontodelete && $sysId == $systemId) {
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=remove_system&remove_system_id='.$sysId.'" class="kundenkarte-system-remove" title="'.$langs->trans('RemoveSystem').'"><i class="fa fa-times"></i></a>';
}
print '</div>';
}
// System hinzufügen Button
if ($permissiontoadd) {
$availableSystems = array_diff_key($allSystems, $customerSystems);
// GLOBAL ausschließen
foreach ($availableSystems as $k => $v) {
if ($v->code === 'GLOBAL') unset($availableSystems[$k]);
}
if (!empty($availableSystems)) {
print '<button type="button" class="button small kundenkarte-add-system-btn" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
print '</button>';
}
}
print '</div>';
// Steuerungs-Buttons (nur in Baumansicht)
$isTreeView = !in_array($action, array('create', 'edit', 'view'));
if ($isTreeView && $systemId > 0) {
print '<div class="kundenkarte-tree-controls">';
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
$showDecomm = getDolGlobalInt('KUNDENKARTE_SHOW_DECOMMISSIONED', 0);
print '<button type="button" class="button small'.($showDecomm ? ' active' : '').'" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
print '<i class="fa '.($showDecomm ? 'fa-eye' : 'fa-eye-slash').'"></i> <span>'.$langs->trans('Decommissioned').'</span>';
print '</button>';
print '</div>';
}
print '</div>'; // End system-tabs-wrapper
// System-Hinzufügen-Formular (versteckt)
if ($permissiontoadd && !empty($availableSystems)) {
print '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="add_system">';
print '<strong>'.$langs->trans('SelectSystemToAdd').':</strong> ';
print '<select name="new_system_id" class="flat">';
print '<option value="">'.$langs->trans('Select').'</option>';
foreach ($availableSystems as $avSys) {
print '<option value="'.$avSys->rowid.'">'.dol_escape_htmltag($avSys->label).'</option>';
}
print '</select>';
print ' <button type="submit" class="button small">'.$langs->trans('Add').'</button>';
print ' <button type="button" class="button small" onclick="document.getElementById(\'add-system-form\').style.display=\'none\';">'.$langs->trans('Cancel').'</button>';
print '</form>';
print '</div>';
}
// Prüfen ob Systeme konfiguriert sind
if (empty($customerSystems)) {
print '<div class="opacitymedium" style="padding:20px;text-align:center;">';
print '<i class="fa fa-info-circle" style="font-size:24px;margin-bottom:10px;"></i><br>';
print $langs->trans('NoSystemsConfigured').'<br><br>';
if ($permissiontoadd && !empty($allSystems)) {
print $langs->trans('ClickAddSystemToStart');
} else {
print $langs->trans('ContactAdminToAddSystems');
}
print '</div>';
} elseif ($systemId > 0) {
// Typen für ausgewähltes System laden
$types = $anlageType->fetchAllBySystem($systemId, 1, 1);
if (in_array($action, array('create', 'edit', 'view'))) {
// Formular oder Detail-Ansicht
if ($action != 'create' && $anlageId > 0) {
$anlage->fetch($anlageId);
$type = new AnlageType($db);
$type->fetch($anlage->fk_anlage_type);
$type->fetchFields();
}
print '<div class="kundenkarte-element-form">';
if ($action == 'view') {
// Detail-Ansicht
print '<h3>'.dol_escape_htmltag($anlage->label).'</h3>';
print '<table class="border centpercent">';
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
// Zugeordnetes Produkt (nur wenn Typ es erlaubt)
if ($type->has_product && $anlage->fk_product > 0) {
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
print '<tr><td>'.$langs->trans('Product').'</td>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$product->id.'">'.dol_escape_htmltag($product->ref).'</a>';
print ' - '.dol_escape_htmltag($product->label);
if ($product->price > 0) {
print ' <span class="opacitymedium">('.price($product->price).' €)</span>';
}
print '</td></tr>';
}
}
// Dynamische Felder
$fieldValues = $anlage->getFieldValues();
$typeFieldsList = $type->fetchFields();
foreach ($typeFieldsList as $field) {
if ($field->field_type === 'header') {
print '<tr class="liste_titre"><th colspan="2" style="padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
} else {
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
if ($value !== '') {
print '<tr><td>'.dol_escape_htmltag($field->field_label).'</td>';
if ($field->field_type === 'date' && $value) {
print '<td>'.dol_print_date(strtotime($value), 'day').'</td></tr>';
} elseif ($field->field_type === 'checkbox') {
print '<td>'.($value ? $langs->trans('Yes') : $langs->trans('No')).'</td></tr>';
} else {
print '<td>'.dol_escape_htmltag($value).'</td></tr>';
}
}
}
}
if ($anlage->note_private) {
print '<tr><td>'.$langs->trans('FieldNotes').'</td>';
print '<td>'.dol_htmlentitiesbr($anlage->note_private).'</td></tr>';
}
// Ausgebaut-Status
if (!empty($anlage->decommissioned)) {
print '<tr><td>'.$langs->trans('Decommissioned').'</td>';
print '<td><span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$langs->trans('Decommissioned').'</span>';
if (!empty($anlage->date_decommissioned)) {
print ' '.dol_print_date(strtotime($anlage->date_decommissioned), 'day');
}
print '</td></tr>';
}
print '</table>';
// Zubehör-Bereich (nur wenn Typ has_accessories hat)
if ($type->has_accessories) {
$accessoryObj = new AnlageAccessory($db);
$accessories = $accessoryObj->fetchAllByAnlage($anlageId);
print '<br><h4><i class="fa fa-puzzle-piece"></i> '.$langs->trans('Accessories').'</h4>';
if (!empty($accessories)) {
print '<div class="div-table-responsive">';
print '<table class="tagtable liste">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('ProductRef').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th class="right">'.$langs->trans('Qty').'</th>';
print '<th>'.$langs->trans('Note').'</th>';
if ($permissiontodelete) {
print '<th class="right">'.$langs->trans('Action').'</th>';
}
print '</tr>';
foreach ($accessories as $acc) {
print '<tr>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$acc->fk_product.'">'.dol_escape_htmltag($acc->product_ref).'</a></td>';
print '<td>'.dol_escape_htmltag($acc->product_label).'</td>';
print '<td class="right">'.$acc->qty.'</td>';
print '<td>'.dol_escape_htmltag($acc->note).'</td>';
if ($permissiontodelete) {
print '<td class="right"><a href="#" class="btn-delete-accessory" data-id="'.$acc->id.'" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a></td>';
}
print '</tr>';
}
print '</table>';
print '</div>';
} else {
print '<p class="opacitymedium">'.$langs->trans('NoAccessories').'</p>';
}
// Zubehör hinzufügen
if ($permissiontoadd) {
print '<div id="add-accessory-form" style="margin-top:10px;">';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<input type="text" id="accessory_product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'...">';
print '<input type="hidden" id="accessory_product_id" value="">';
print '<input type="number" id="accessory_qty" class="flat" value="1" min="1" style="width:80px;">';
print '<button type="button" id="btn-add-accessory" class="button small" disabled>'.$langs->trans('Add').'</button>';
print '</div>';
print '</div>';
}
// Bestellfunktion
if ($permissiontoadd && !empty($accessories)) {
print '<div style="margin-top:15px;padding:10px;border:1px solid #444;border-radius:4px;">';
print '<h5 style="margin:0 0 10px 0;"><i class="fa fa-shopping-cart"></i> '.$langs->trans('OrderAccessories').'</h5>';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<select id="supplier_select" class="flat minwidth200">';
print '<option value="">'.$langs->trans('SelectSupplier').'</option>';
// Lieferanten laden
$sqlSupp = "SELECT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s WHERE s.fournisseur = 1 AND s.status = 1 ORDER BY s.nom";
$resSupp = $db->query($sqlSupp);
if ($resSupp) {
while ($objSupp = $db->fetch_object($resSupp)) {
print '<option value="'.$objSupp->rowid.'">'.dol_escape_htmltag($objSupp->nom).'</option>';
}
}
print '</select>';
print '<button type="button" id="btn-order-accessories" class="button small">'.$langs->trans('CreateSupplierOrder').'</button>';
print '</div>';
print '</div>';
}
}
// Aktions-Buttons
print '<div class="tabsAction">';
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=edit&anlage_id='.$anlageId.'">'.$langs->trans('Modify').'</a>';
}
if ($permissiontodelete) {
print '<a class="butActionDelete" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=delete&anlage_id='.$anlageId.'">'.$langs->trans('Delete').'</a>';
}
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'">'.$langs->trans('Back').'</a>';
print '</div>';
} else {
// Erstellen/Bearbeiten-Formular
$isEdit = ($action == 'edit');
$formAction = $isEdit ? 'update' : 'add';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="'.$formAction.'">';
print '<input type="hidden" name="system" value="'.$systemId.'">';
if ($isEdit) {
print '<input type="hidden" name="anlage_id" value="'.$anlageId.'">';
}
print '<table class="border centpercent" id="element_form_table">';
// Label
$labelValue = $isEdit ? $anlage->label : GETPOST('label');
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Label').'</td>';
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($labelValue).'" required></td></tr>';
// Typ
print '<tr><td class="fieldrequired">'.$langs->trans('Type').'</td>';
print '<td><select name="fk_anlage_type" class="flat minwidth200" id="select_type" required>';
print '<option value="">'.$langs->trans('SelectType').'</option>';
foreach ($types as $t) {
$selected = ($isEdit && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
print '<option value="'.$t->id.'" data-has-product="'.($t->has_product ? '1' : '0').'" data-has-accessories="'.($t->has_accessories ? '1' : '0').'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
print '</select>';
if (empty($types)) {
print '<br><span class="warning">'.$langs->trans('NoTypesDefinedForSystem').' <a href="'.dol_buildpath('/kundenkarte/admin/anlage_types.php', 1).'">'.$langs->trans('GoToTypeAdmin').'</a></span>';
}
print '</td></tr>';
// Übergeordnetes Element
$tree = $anlage->fetchTree($socId, $systemId);
$selectedParent = $isEdit ? $anlage->fk_parent : $parentId;
$excludeId = $isEdit ? $anlageId : 0;
print '<tr><td>'.$langs->trans('SelectParent').'</td>';
print '<td><select name="fk_parent" class="flat minwidth200">';
print '<option value="0">('.$langs->trans('Root').')</option>';
werkzeuge_printTreeOptions($tree, $selectedParent, $excludeId);
print '</select></td></tr>';
// Produkt-Zuordnung (wird per JS ein-/ausgeblendet je nach Typ)
$productValue = '';
$productId = 0;
if ($isEdit && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
$productValue = $product->ref.' - '.$product->label;
$productId = $product->id;
}
}
print '<tr id="row_product" style="display:none;"><td>'.$langs->trans('Product').'</td>';
print '<td>';
print '<input type="text" id="product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'..." value="'.dol_escape_htmltag($productValue).'">';
print '<input type="hidden" name="fk_product" id="fk_product" value="'.$productId.'">';
print '</td></tr>';
// Dynamische Felder werden per JS geladen
print '<tbody id="dynamic_fields"></tbody>';
// Notizen
print '<tr><td>'.$langs->trans('FieldNotes').'</td>';
$noteValue = $isEdit ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : '');
print '<td><textarea name="note_private" class="flat minwidth300" rows="3">'.htmlspecialchars($noteValue, ENT_QUOTES, 'UTF-8').'</textarea></td></tr>';
print '</table>';
print '<div class="center" style="margin-top:20px;">';
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '</form>';
}
print '</div>';
} else {
// Baumansicht
if ($permissiontoadd) {
print '<div style="margin-bottom:15px;">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=create">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
print '</a>';
print '</div>';
}
// Baum laden
$tree = $anlage->fetchTree($socId, $systemId);
// Feld-Metadaten laden
$typeFieldsMap = array();
$sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
if (!isset($typeFieldsMap[$obj->fk_anlage_type])) {
$typeFieldsMap[$obj->fk_anlage_type] = array();
}
$typeFieldsMap[$obj->fk_anlage_type][] = $obj;
}
$db->free($resql);
}
if (!empty($tree)) {
print '<div class="kundenkarte-tree'.($showDecomm ? ' show-decommissioned' : '').'" data-system="'.$systemId.'" data-socid="'.$socId.'">';
werkzeuge_printTree($tree, $socId, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap);
print '</div>';
} else {
print '<div class="opacitymedium" style="padding:20px;text-align:center;">';
print '<i class="fa fa-wrench" style="font-size:48px;margin-bottom:15px;color:#666;"></i><br>';
print $langs->trans('NoToolsYet').'<br><br>';
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=create"><i class="fa fa-plus"></i> '.$langs->trans('AddFirstTool').'</a>';
}
print '</div>';
}
}
} // Ende elseif ($systemId > 0)
print '</div>'; // fichecenter
// Tooltip Container
print '<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>';
// JavaScript: Produkt-Autocomplete + Zubehör-AJAX
print '<script>
$(document).ready(function() {
var baseUrl = "'.dol_escape_js(dol_buildpath('/kundenkarte', 1)).'";
// Produkt-Zeile ein-/ausblenden je nach Typ-Flag has_product
var $typeSelect = $("#select_type");
function updateProductRow() {
var $selected = $typeSelect.find("option:selected");
var hasProduct = $selected.data("has-product");
if (hasProduct == 1) {
$("#row_product").show();
} else {
$("#row_product").hide();
$("#fk_product").val("");
$("#product_search").val("");
}
}
$typeSelect.on("change", updateProductRow);
updateProductRow(); // Initial
// Produkt-Autocomplete
function initProductAutocomplete(inputSelector, hiddenSelector) {
var $input = $(inputSelector);
var $hidden = $(hiddenSelector);
if (!$input.length) return;
var searchTimeout;
$input.on("input", function() {
clearTimeout(searchTimeout);
var term = $(this).val();
if (term.length < 2) {
$hidden.val("");
$(".product-autocomplete-dropdown").remove();
return;
}
searchTimeout = setTimeout(function() {
$.get(baseUrl + "/ajax/equipment.php", {
action: "get_products",
term: term,
token: $("input[name=token]").val() || ""
}, function(data) {
$(".product-autocomplete-dropdown").remove();
if (data.success && data.products && data.products.length > 0) {
var $dropdown = $("<div class=\"product-autocomplete-dropdown\"></div>");
$.each(data.products, function(i, p) {
var label = p.ref + " - " + p.label;
if (p.price > 0) label += " (" + p.price + " €)";
$dropdown.append(
$("<div class=\"product-autocomplete-item\"></div>")
.text(label)
.data("id", p.id)
.data("ref", p.ref)
.data("label", p.label)
);
});
$input.after($dropdown);
$dropdown.on("click", ".product-autocomplete-item", function() {
var id = $(this).data("id");
var ref = $(this).data("ref");
var label = $(this).data("label");
$input.val(ref + " - " + label);
$hidden.val(id);
$dropdown.remove();
// Zubehör: Button aktivieren
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", false);
}
});
}
});
}, 300);
});
// Dropdown schließen bei Klick außerhalb
$(document).on("click", function(e) {
if (!$(e.target).closest(inputSelector + ", .product-autocomplete-dropdown").length) {
$(".product-autocomplete-dropdown").remove();
}
});
// Bei manuellem Leeren auch Hidden zurücksetzen
$input.on("change", function() {
if (!$(this).val()) {
$hidden.val("");
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", true);
}
}
});
}
initProductAutocomplete("#product_search", "#fk_product");
initProductAutocomplete("#accessory_product_search", "#accessory_product_id");
// Zubehör hinzufügen
$("#btn-add-accessory").on("click", function() {
var productId = $("#accessory_product_id").val();
var qty = $("#accessory_qty").val() || 1;
var anlageId = '.((int) $anlageId).';
if (!productId || !anlageId) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "add",
fk_anlage: anlageId,
fk_product: productId,
qty: qty,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) {
location.reload();
} else {
alert(data.error || "Fehler");
}
}).fail(function() {
alert("Server-Fehler");
});
});
// Zubehör löschen
$(".btn-delete-accessory").on("click", function(e) {
e.preventDefault();
if (!confirm("'.$langs->trans('ConfirmDeleteAccessory').'")) return;
var accId = $(this).data("id");
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "delete",
id: accId,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) {
location.reload();
} else {
alert(data.error || "Fehler");
}
});
});
// Lieferantenbestellung
$("#btn-order-accessories").on("click", function() {
var supplierId = $("#supplier_select").val();
var anlageId = '.((int) $anlageId).';
if (!supplierId) {
alert("Bitte Lieferant auswählen");
return;
}
// Alle Zubehör-IDs sammeln
var ids = [];
$(".btn-delete-accessory").each(function() {
ids.push($(this).data("id"));
});
if (ids.length === 0) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "order",
fk_anlage: anlageId,
supplier_id: supplierId,
"ids[]": ids,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success && data.order_id) {
window.location.href = "'.DOL_URL_ROOT.'/fourn/commande/card.php?id=" + data.order_id;
} else {
alert(data.error || "Fehler beim Erstellen der Bestellung");
}
}).fail(function() {
alert("Server-Fehler");
});
});
});
</script>';
// CSS für Autocomplete
print '<style>
.product-autocomplete-dropdown {
position: absolute;
z-index: 1000;
background: var(--colorbackbody, #fff);
border: 1px solid #555;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.product-autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #333;
}
.product-autocomplete-item:hover {
background: var(--colorbacklinepairhover, #333);
}
.product-autocomplete-item:last-child {
border-bottom: none;
}
</style>';
llxFooter();
$db->close();
/**
* Baum rekursiv ausgeben (vereinfachte Version für Werkzeuge-Seite)
*/
function werkzeuge_printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$fieldValues = $node->getFieldValues();
// Badges sammeln
$treeInfoBadges = array();
$treeInfoParentheses = array();
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
if ($fieldDef->field_type === 'header') continue;
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
if ($fieldDef->show_in_tree && $value !== '') {
$displayVal = $value;
if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day');
}
$fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
'type' => $fieldDef->field_type,
'color' => $fieldDef->badge_color ?? ''
);
$displayMode = $fieldDef->tree_display_mode ?? 'badge';
if ($displayMode === 'parentheses') {
$treeInfoParentheses[] = $fieldInfo;
} else {
$treeInfoBadges[] = $fieldInfo;
}
}
}
}
$nodeClass = 'kundenkarte-tree-node';
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
if ($node->type_can_have_children) {
$nodeClass .= ' node-structure';
} else {
$nodeClass .= ' node-leaf';
}
print '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
// Toggle
if ($hasChildren) {
print '<span class="kundenkarte-tree-toggle"><i class="fa fa-chevron-down"></i></span>';
} else {
print '<span class="kundenkarte-tree-toggle" style="visibility:hidden;"><i class="fa fa-chevron-down"></i></span>';
}
// Icon
$picto = $node->type_picto ? $node->type_picto : 'fa-wrench';
print '<span class="kundenkarte-tree-icon">'.kundenkarte_render_icon($picto).'</span>';
// Label
$viewUrl = $_SERVER['PHP_SELF'].'?system='.$systemId.'&action=view&anlage_id='.$node->id;
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
if (!empty($treeInfoParentheses)) {
$infoValues = array();
foreach ($treeInfoParentheses as $info) {
$infoValues[] = dol_escape_htmltag($info['value']);
}
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
}
print '</span>';
// Ausgebaut-Badge
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Produkt-Badge
if (!empty($node->fk_product) && !empty($node->product_ref)) {
print ' <span class="badge badge-secondary"><i class="fa fa-cube"></i> '.dol_escape_htmltag($node->product_ref).'</span>';
}
// Spacer
print '<span class="kundenkarte-tree-spacer"></span>';
// Badges
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
if (!empty($treeInfoBadges)) {
print '<span class="kundenkarte-tree-badges">';
foreach ($treeInfoBadges as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
print '</span>';
}
print '</span>';
}
// Typ-Badge
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '<span class="kundenkarte-tree-type badge badge-secondary">'.dol_escape_htmltag($typeDisplay).'</span>';
}
// Aktionen
print '<span class="kundenkarte-tree-actions">';
print '<a href="'.$viewUrl.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
}
print '</span>';
print '</div>';
// Kinder
if ($hasChildren) {
print '<div class="kundenkarte-tree-children">';
werkzeuge_printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap);
print '</div>';
}
print '</div>';
}
}
/**
* Baum-Options für Select (vereinfacht)
*/
function werkzeuge_printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '', $level = 0)
{
foreach ($nodes as $node) {
if ($node->id == $excludeId) continue;
$sel = ($node->id == $selected) ? ' selected' : '';
print '<option value="'.$node->id.'"'.$sel.'>'.$prefix.dol_escape_htmltag($node->label).'</option>';
if (!empty($node->children)) {
werkzeuge_printTreeOptions($node->children, $selected, $excludeId, $prefix.'── ', $level + 1);
}
}
}