Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 785bb9f66f | |||
| b8b2655cd0 | |||
| 71272fa425 | |||
| 16e51a799a | |||
| 8826c286ef | |||
| 4181efacdb | |||
| 848232c5a6 | |||
| 95e1860940 | |||
| 6b3b6d7e95 | |||
| 4540b8c595 | |||
| be3a53e77e | |||
| 89a4db4d21 | |||
| 50ae4e4a08 | |||
| 5f23727202 | |||
| 7de0349808 | |||
| 65f24495e6 | |||
| c4338c8d7a | |||
| 3b9daeb238 | |||
| 7a548c87e2 | |||
| a14b33b7c7 |
41 changed files with 10002 additions and 508 deletions
209
CLAUDE.md
209
CLAUDE.md
|
|
@ -99,14 +99,62 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte.
|
|||
|
||||
## Dateistruktur
|
||||
|
||||
### Tabs
|
||||
- `tabs/anlagen.php` - Hauptansicht für Anlagen auf Kundenebene
|
||||
- `tabs/contact_anlagen.php` - Anlagen für Kontakte
|
||||
- `tabs/favoriteproducts.php` - Lieblingsprodukte auf Kundenebene
|
||||
- `tabs/contact_favoriteproducts.php` - Lieblingsprodukte für Kontakte
|
||||
|
||||
### Admin
|
||||
- `admin/anlage_types.php` - Verwaltung der Element-Typen
|
||||
- `ajax/` - AJAX-Endpunkte für dynamische Funktionen
|
||||
- `js/kundenkarte.js` - Alle JavaScript-Komponenten
|
||||
- `css/kundenkarte.css` - Alle Styles (Dark Mode)
|
||||
- `admin/building_types.php` - Verwaltung der Gebäude-Typen
|
||||
- `admin/equipment_types.php` - Verwaltung der Equipment-Typen
|
||||
- `admin/setup.php` - Modul-Einstellungen
|
||||
|
||||
### Klassen (class/)
|
||||
- `anlage.class.php` - Haupt-Anlage-Klasse
|
||||
- `anlagetype.class.php` - Element-Typen (fetchAllBySystem mit color!)
|
||||
- `buildingtype.class.php` - Gebäude-Typen
|
||||
- `anlageaccessory.class.php` - Zubehör mit CRUD + Lieferantenbestellung
|
||||
- `anlageconnection.class.php` - Kabelverbindungen (Anlagen-Ebene)
|
||||
- `anlagefile.class.php` - Datei-Anhänge
|
||||
- `anlagebackup.class.php` - Backup/Restore
|
||||
- `auditlog.class.php` - Änderungsprotokoll
|
||||
- `equipment.class.php` - Equipment-Instanzen auf Hutschienen
|
||||
- `equipmenttype.class.php` - Equipment-Typ-Vorlagen (LS, FI, Neozed etc.)
|
||||
- `equipmentcarrier.class.php` - Hutschienen (DIN-Rails)
|
||||
- `equipmentpanel.class.php` - Schaltschrankfelder (Panels)
|
||||
- `equipmentconnection.class.php` - Verbindungen im Schaltplan-Editor
|
||||
- `terminalbridge.class.php` - Terminal-Brücken
|
||||
- `mediumtype.class.php` - Leitungstypen
|
||||
- `busbartype.class.php` - Sammelschienen-Typen
|
||||
|
||||
### Libraries (lib/)
|
||||
- `kundenkarte.lib.php` - Allgemeine Hilfs-Funktionen
|
||||
- `graph_view.lib.php` - Shared Graph-Funktionen (Toolbar, Container, Legende)
|
||||
- `wiring_diagram.lib.php` - Leitungslaufplan + 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
|
||||
|
|
|
|||
153
ChangeLog.md
153
ChangeLog.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
154
ajax/anlage_accessory.php
Executable file
154
ajax/anlage_accessory.php
Executable 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
71
ajax/busbar_types.php
Executable 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);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
60
ajax/export_wiring_diagram_pdf.php
Normal file
60
ajax/export_wiring_diagram_pdf.php
Normal 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');
|
||||
74
ajax/pwa_api.php
Normal file → Executable file
74
ajax/pwa_api.php
Normal file → Executable 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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -96,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,";
|
||||
|
|
@ -110,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");
|
||||
|
|
@ -203,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;
|
||||
|
|
@ -231,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;
|
||||
|
|
@ -291,6 +300,7 @@ 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);
|
||||
|
|
@ -407,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)
|
||||
|
|
@ -417,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";
|
||||
|
|
@ -717,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)
|
||||
|
|
@ -727,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
391
class/anlageaccessory.class.php
Executable 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.4';
|
||||
$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 */
|
||||
|
||||
|
||||
|
|
@ -636,6 +653,18 @@ class modKundenKarte extends DolibarrModules
|
|||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -950,6 +979,141 @@ class modKundenKarte extends DolibarrModules
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
40
css/pwa.css
Normal file → Executable file
40
css/pwa.css
Normal file → Executable 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
0
img/pwa-icon-192.png
Normal file → Executable 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
0
img/pwa-icon-512.png
Normal file → Executable file
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
4508
js/kundenkarte.js
4508
js/kundenkarte.js
File diff suppressed because it is too large
Load diff
574
js/pwa.js
Normal file → Executable file
574
js/pwa.js
Normal file → Executable 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)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -561,3 +561,22 @@ 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
|
||||
|
|
|
|||
|
|
@ -309,3 +309,22 @@ 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
|
||||
|
|
|
|||
2131
lib/wiring_diagram.lib.php
Normal file
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
0
manifest.json
Normal file → Executable file
40
pwa.php
Normal file → Executable file
40
pwa.php
Normal file → Executable 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
0
pwa_auth.php
Normal file → Executable file
12
sql/llx_kundenkarte_anlage_accessory.key.sql
Executable file
12
sql/llx_kundenkarte_anlage_accessory.key.sql
Executable 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;
|
||||
19
sql/llx_kundenkarte_anlage_accessory.sql
Executable file
19
sql/llx_kundenkarte_anlage_accessory.sql
Executable 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
7
sw.js
Normal file → Executable 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));
|
||||
|
|
|
|||
320
tabs/anlagen.php
320
tabs/anlagen.php
|
|
@ -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,8 +510,9 @@ 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>';
|
||||
print '<button type="button" class="button small" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
|
||||
print '<i class="fa fa-eye-slash"></i> <span>'.$langs->trans('Decommissioned').'</span>';
|
||||
$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;
|
||||
|
|
@ -590,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 !== '') {
|
||||
|
|
@ -762,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>';
|
||||
|
|
@ -773,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>';
|
||||
|
|
@ -797,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) {
|
||||
|
|
@ -896,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>';
|
||||
}
|
||||
|
|
@ -908,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>';
|
||||
}
|
||||
|
|
@ -929,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>';
|
||||
|
||||
|
|
@ -1018,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() {
|
||||
|
|
@ -1035,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();
|
||||
|
|
@ -1105,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 {
|
||||
|
|
@ -1122,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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,8 +508,9 @@ 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>';
|
||||
print '<button type="button" class="button small" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
|
||||
print '<i class="fa fa-eye-slash"></i> <span>'.$langs->trans('Decommissioned').'</span>';
|
||||
$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;
|
||||
|
|
@ -588,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 !== '') {
|
||||
|
|
@ -760,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>';
|
||||
|
|
@ -771,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>';
|
||||
|
|
@ -795,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) {
|
||||
|
|
@ -894,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>';
|
||||
}
|
||||
|
|
@ -906,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>';
|
||||
}
|
||||
|
|
@ -927,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>';
|
||||
|
||||
|
|
@ -1016,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() {
|
||||
|
|
@ -1033,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();
|
||||
|
|
@ -1103,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 {
|
||||
|
|
@ -1120,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();
|
||||
|
||||
|
|
|
|||
982
werkzeuge.php
Executable file
982
werkzeuge.php
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue