feat(v8.6): Räumlichkeit, Verteilungs-Tabellen, Bundled-Terminals, PWA-Updates
- output_location (Räumlichkeit): Neues Textfeld am Abgang für Raum/Ort des Verbrauchers. DB-Migration, Backend (AJAX), Frontend (Website + PWA), Anzeige im Schaltplan (kursiv) und in PDF-Tabellen. - Verteilungs-Tabellen: Kundenansicht (A4, Nr/Verbraucher/Räumlichkeit) und Technikeransicht (A4, R.Klem/FI/Nr/Verbraucher/Räumlichkeit/Typ) im Leitungslaufplan-PDF. Gruppiert nach Feld/Reihe mit automatischem Seitenumbruch. - Bundled-Terminals Checkbox: Im Website-Abgang-Dialog (war vorher nur PWA). - PWA: Diverse Verbesserungen, Service Worker v12.4, Connection-Modal erweitert. - Typ-Flags: has_product auch für Gebäudetypen, Equipment-Typ Erweiterungen. - CLAUDE.md + Doku aktualisiert. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8826c286ef
commit
16e51a799a
30 changed files with 1875 additions and 435 deletions
44
CLAUDE.md
44
CLAUDE.md
|
|
@ -132,7 +132,7 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte.
|
|||
### Libraries (lib/)
|
||||
- `kundenkarte.lib.php` - Allgemeine Hilfs-Funktionen
|
||||
- `graph_view.lib.php` - Shared Graph-Funktionen (Toolbar, Container, Legende)
|
||||
- `wiring_diagram.lib.php` - Leitungslaufplan (WiringDiagramAnalyzer + WiringDiagramRenderer)
|
||||
- `wiring_diagram.lib.php` - Leitungslaufplan + Verteilungs-Tabellen (~2.130 Zeilen)
|
||||
|
||||
### AJAX-Endpunkte (ajax/) — 30+ Dateien
|
||||
- `anlage.php` - Anlagen CRUD
|
||||
|
|
@ -150,9 +150,9 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte.
|
|||
- `pwa_api.php` - PWA-Endpoints
|
||||
|
||||
### Frontend
|
||||
- `js/kundenkarte.js` - Haupt-JS (~11.000 Zeilen)
|
||||
- `js/kundenkarte.js` - Haupt-JS (~15.600 Zeilen)
|
||||
- `js/kundenkarte_cytoscape.js` - Graph-JS (~900 Zeilen)
|
||||
- `js/pwa.js` - PWA-JS (~1.950 Zeilen)
|
||||
- `js/pwa.js` - PWA-JS (~3.400 Zeilen)
|
||||
- `css/kundenkarte.css` - Alle Styles (Dark Mode Theme)
|
||||
- `css/pwa.css` - PWA-Styles
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
|
|||
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
|
||||
- `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter)
|
||||
- `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen
|
||||
- `sw.js` - Service Worker für Offline-Cache (v6.1)
|
||||
- `sw.js` - Service Worker für Offline-Cache (v12.4)
|
||||
- `manifest.json` - Web App Manifest für Installation
|
||||
|
||||
### Workflow
|
||||
|
|
@ -260,7 +260,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
|
|||
- `bundled_terminals = 'all'` in Connection bedeutet: Alle Terminals belegt
|
||||
- Im Editor: Ein Pfeil spannt über alle Terminals des Equipment
|
||||
- Label wird zentriert über alle Terminals angezeigt
|
||||
- Checkbox "Alle bündeln" nur bei Equipment mit >1 Terminal sichtbar
|
||||
- Checkbox "Alle Terminals bündeln" im Abgang-Dialog (Website + PWA), nur bei Equipment mit >1 Terminal
|
||||
|
||||
### Terminal-Konfiguration (v7.5)
|
||||
- `terminals_config` JSON im Equipment-Typ definiert Terminal-Positionen
|
||||
|
|
@ -348,16 +348,18 @@ Normgerechter Stromlaufplan in aufgelöster Darstellung (DIN EN 61082) als PDF-E
|
|||
**Komplett separates Feature** — kann durch Löschen von 2 Dateien + 8 Zeilen rückstandsfrei entfernt werden.
|
||||
|
||||
### Dateien
|
||||
- `lib/wiring_diagram.lib.php` — Kernlogik (~1240 Zeilen)
|
||||
- `lib/wiring_diagram.lib.php` — Kernlogik (~2.130 Zeilen)
|
||||
- `WiringDiagramAnalyzer` — Lädt Daten, baut Phase-Map (PHP-Port), tracet Strompfade
|
||||
- `WiringDiagramRenderer` — Zeichnet PDF mit TCPDF
|
||||
- `ajax/export_wiring_diagram_pdf.php` — Endpoint
|
||||
- Buttons in `tabs/anlagen.php` + `tabs/contact_anlagen.php` (je 4 Zeilen)
|
||||
|
||||
### PDF-Inhalt (3 Teile)
|
||||
1. **Leitungslaufplan** — L1/L2/L3 horizontal oben, vertikale Strompfade pro Abgang, FI/RCD + LS-Symbole, Abgang-Pfeile, N/PE unten
|
||||
2. **Abgangsverzeichnis** — Tabelle pro Hutschiene mit: Abg.Nr, Bezeichnung, Phase, Absicherung, Kabel, Schutzgerät
|
||||
3. **Legende** — Phasenfarben DIN VDE, VDE-Symbole, Norm-Referenzen
|
||||
### PDF-Inhalt (5 Teile)
|
||||
1. **Leitungslaufplan** (A3 quer) — L1/L2/L3 horizontal oben, vertikale Strompfade pro Abgang, FI/RCD + LS-Symbole, Abgang-Pfeile, N/PE unten
|
||||
2. **Abgangsverzeichnis** (A3 quer) — Tabelle pro Hutschiene mit: Abg.Nr, Bezeichnung, Phase, Absicherung, Kabel, Schutzgerät
|
||||
3. **Kundenansicht** (A4 hoch) — `renderKundenansicht()` — Einfache Tabelle: Nr | Verbraucher | Räumlichkeit, gruppiert nach Feld/Reihe
|
||||
4. **Technikeransicht** (A4 hoch) — `renderTechnikeransicht()` — Erweiterte Tabelle: R.Klem | FI | Nr | Verbraucher | Räumlichkeit | Typ
|
||||
5. **Mini-Legende** — Phasenfarben DIN VDE auf Seite 1 unten links
|
||||
|
||||
### Abgangsnummer-Format
|
||||
`R{Reihe}.{Position}` z.B. `R1.3` = Carrier-Position 1, Equipment-TE-Position 3
|
||||
|
|
@ -380,6 +382,28 @@ Pro Abgang (Connection mit `fk_target = NULL`):
|
|||
- FI/RCD: Rechteck mit Kreis + Vertikallinie (Differenzstrom-Symbol)
|
||||
- Gezeichnet mit TCPDF-Primitiven (Line, Rect, Circle, Polygon)
|
||||
|
||||
## Räumlichkeit / output_location (v8.6)
|
||||
|
||||
### Übersicht
|
||||
Zusätzliches Textfeld am Abgang (Output-Connection) für den Raum/Ort des Verbrauchers (z.B. "Küche", "Bad OG").
|
||||
|
||||
### Datenbank
|
||||
- Spalte `output_location` (varchar 255) in `llx_kundenkarte_equipment_connection`
|
||||
- Migration: `migrate_v1110_output_location()` in `modKundenKarte.class.php`
|
||||
|
||||
### Backend
|
||||
- `EquipmentConnection::$output_location` — Property, in create/update/fetch
|
||||
- `ajax/equipment_connection.php` — create_output + update + list_all
|
||||
- `ajax/pwa_api.php` — get_carrier_equipment + create_connection + update_connection
|
||||
|
||||
### Frontend
|
||||
- **Website**: Eingabefeld im `renderAbgangDialog()` (kundenkarte.js)
|
||||
- **PWA**: Eingabefeld `#conn-location` im Connection-Modal (pwa.php + pwa.js)
|
||||
- **Anzeige im Schaltplan**:
|
||||
- Website SVG: `<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
|
||||
|
|
|
|||
0
ChangeLog.md
Normal file → Executable file
0
ChangeLog.md
Normal file → Executable file
0
admin/anlage_types.php
Normal file → Executable file
0
admin/anlage_types.php
Normal file → Executable file
0
admin/building_types.php
Normal file → Executable file
0
admin/building_types.php
Normal file → Executable file
0
admin/equipment_types.php
Normal file → Executable file
0
admin/equipment_types.php
Normal file → Executable file
0
admin/setup.php
Normal file → Executable file
0
admin/setup.php
Normal file → Executable file
0
ajax/anlage_accessory.php
Normal file → Executable file
0
ajax/anlage_accessory.php
Normal file → Executable file
|
|
@ -267,6 +267,7 @@ switch ($action) {
|
|||
'block_color' => $eq->getBlockColor(),
|
||||
'field_values' => $eq->getFieldValues(),
|
||||
'fk_product' => $eq->fk_product,
|
||||
'fk_protection' => $eq->fk_protection,
|
||||
'product_ref' => $productRef,
|
||||
'product_label' => $productLabel
|
||||
);
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ switch ($action) {
|
|||
if (GETPOSTISSET('connection_type')) $connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||
if (GETPOSTISSET('color')) $connection->color = GETPOST('color', 'alphanohtml');
|
||||
if (GETPOSTISSET('output_label')) $connection->output_label = GETPOST('output_label', 'alphanohtml');
|
||||
if (GETPOSTISSET('output_location')) $connection->output_location = GETPOST('output_location', 'alphanohtml');
|
||||
if (GETPOSTISSET('medium_type')) $connection->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||
|
|
@ -303,6 +304,7 @@ switch ($action) {
|
|||
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||
$connection->color = GETPOST('color', 'alphanohtml');
|
||||
$connection->output_label = GETPOST('output_label', 'alphanohtml');
|
||||
$connection->output_location = GETPOST('output_location', 'alphanohtml');
|
||||
$connection->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||
|
|
@ -367,6 +369,7 @@ switch ($action) {
|
|||
'connection_type' => $obj->connection_type,
|
||||
'color' => $obj->color ?: '#3498db',
|
||||
'output_label' => $obj->output_label,
|
||||
'output_location' => isset($obj->output_location) ? $obj->output_location : null,
|
||||
'medium_type' => $obj->medium_type,
|
||||
'medium_spec' => $obj->medium_spec,
|
||||
'medium_length' => $obj->medium_length,
|
||||
|
|
|
|||
70
ajax/pwa_api.php
Normal file → Executable file
70
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;
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
0
class/anlageaccessory.class.php
Normal file → Executable file
0
class/anlageaccessory.class.php
Normal file → Executable file
0
class/anlagetype.class.php
Normal file → Executable file
0
class/anlagetype.class.php
Normal file → Executable file
0
class/buildingtype.class.php
Normal file → Executable file
0
class/buildingtype.class.php
Normal file → Executable file
|
|
@ -30,6 +30,7 @@ class EquipmentConnection extends CommonObject
|
|||
|
||||
// Output/endpoint info
|
||||
public $output_label;
|
||||
public $output_location; // Räumlichkeit/Örtlichkeit des Verbrauchers
|
||||
|
||||
// Medium info (cable, wire, etc.)
|
||||
public $medium_type;
|
||||
|
|
@ -91,7 +92,7 @@ class EquipmentConnection extends CommonObject
|
|||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||
$sql .= "entity, fk_source, source_terminal, source_terminal_id, bundled_terminals, fk_target, target_terminal, target_terminal_id,";
|
||||
$sql .= " connection_type, color, output_label,";
|
||||
$sql .= " connection_type, color, output_label, output_location,";
|
||||
$sql .= " medium_type, medium_spec, medium_length,";
|
||||
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, num_lines, fk_busbar_type, fk_carrier, position_y, path_data,";
|
||||
$sql .= " note_private, status, date_creation, fk_user_creat";
|
||||
|
|
@ -107,6 +108,7 @@ class EquipmentConnection extends CommonObject
|
|||
$sql .= ", ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
|
||||
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||
$sql .= ", ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
|
||||
$sql .= ", ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL");
|
||||
$sql .= ", ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
|
||||
$sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||
$sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||
|
|
@ -178,6 +180,7 @@ class EquipmentConnection extends CommonObject
|
|||
$this->connection_type = $obj->connection_type;
|
||||
$this->color = $obj->color;
|
||||
$this->output_label = $obj->output_label;
|
||||
$this->output_location = isset($obj->output_location) ? $obj->output_location : null;
|
||||
$this->medium_type = $obj->medium_type;
|
||||
$this->medium_spec = $obj->medium_spec;
|
||||
$this->medium_length = $obj->medium_length;
|
||||
|
|
@ -234,6 +237,7 @@ class EquipmentConnection extends CommonObject
|
|||
$sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
|
||||
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||
$sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
|
||||
$sql .= ", output_location = ".($this->output_location ? "'".$this->db->escape($this->output_location)."'" : "NULL");
|
||||
$sql .= ", medium_type = ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
|
||||
$sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||
$sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||
|
|
|
|||
|
|
@ -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 = '9.7';
|
||||
$this->version = '11.0.8';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -662,6 +662,9 @@ class modKundenKarte extends DolibarrModules
|
|||
|
||||
// v11.0.0: Busbar type reference and num_lines for connections
|
||||
$this->migrate_v1100_busbar_fields();
|
||||
|
||||
// v11.1.0: Räumlichkeit (output_location) für Abgänge
|
||||
$this->migrate_v1110_output_location();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1093,6 +1096,24 @@ class modKundenKarte extends DolibarrModules
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration v11.1.0: Räumlichkeit (output_location) für Abgänge
|
||||
*/
|
||||
private function migrate_v1110_output_location()
|
||||
{
|
||||
$table = MAIN_DB_PREFIX."kundenkarte_equipment_connection";
|
||||
|
||||
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'output_location'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN output_location varchar(255) DEFAULT NULL AFTER output_label");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when module is disabled.
|
||||
* Remove from database constants, boxes and permissions from Dolibarr database.
|
||||
|
|
|
|||
|
|
@ -2151,10 +2151,30 @@ body.kundenkarte-drag-active * {
|
|||
.schematic-editor-actions {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 10px !important;
|
||||
gap: 5px !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Einheitliche Größe für alle Toolbar-Buttons (button + a) */
|
||||
.schematic-editor-actions > button,
|
||||
.schematic-editor-actions > a {
|
||||
padding: 5px 8px !important;
|
||||
background: #333 !important;
|
||||
border: 1px solid #555 !important;
|
||||
border-radius: 3px !important;
|
||||
cursor: pointer !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.4 !important;
|
||||
height: 30px !important;
|
||||
box-sizing: border-box !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 4px !important;
|
||||
text-decoration: none !important;
|
||||
white-space: nowrap !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.schematic-editor-toggle {
|
||||
color: #3498db !important;
|
||||
text-decoration: none !important;
|
||||
|
|
|
|||
21
css/pwa.css
Normal file → Executable file
21
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 {
|
||||
|
|
@ -1029,6 +1033,15 @@ body {
|
|||
background: rgba(173, 140, 79, 0.4);
|
||||
}
|
||||
|
||||
.terminal-label .output-location {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-size: 8px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.terminal-label .cable-info {
|
||||
font-weight: normal;
|
||||
font-size: 7px;
|
||||
|
|
@ -2082,3 +2095,9 @@ body {
|
|||
border: 2px dashed rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Leere TE-Positionen ohne Terminal (z.B. NEO 4TE aber nur 3 Terminals) */
|
||||
.terminal-point.no-terminal {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
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 |
|
|
@ -1504,7 +1504,7 @@
|
|||
data.fields.forEach(function(field) {
|
||||
if (field.type === 'header') {
|
||||
// Header row spans both columns with styling
|
||||
html += '<tr class="liste_titre dynamic-field-row"><th colspan="2" style="background:#f0f0f0;padding:8px;">' + KundenKarte.DynamicFields.escapeHtml(field.label) + '</th></tr>';
|
||||
html += '<tr class="liste_titre dynamic-field-row"><th colspan="2" style="padding:8px;">' + KundenKarte.DynamicFields.escapeHtml(field.label) + '</th></tr>';
|
||||
} else {
|
||||
html += '<tr class="dynamic-field-row"><td class="titlefield">' + KundenKarte.DynamicFields.escapeHtml(field.label);
|
||||
if (field.required) html += ' <span class="fieldrequired">*</span>';
|
||||
|
|
@ -4284,11 +4284,17 @@
|
|||
|
||||
// Label if exists - positioned along the routing path
|
||||
if (conn.output_label) {
|
||||
var fullLabel = conn.output_label;
|
||||
if (conn.output_location) fullLabel += ' · ' + conn.output_location;
|
||||
var labelY = equipmentY + 35 + (connectionIndex * 12);
|
||||
var labelX = (sourceX + targetX) / 2;
|
||||
html += '<rect x="' + (labelX - 25) + '" y="' + (labelY - 8) + '" width="50" height="12" rx="2" fill="#1e1e1e" opacity="0.9"/>';
|
||||
var rectWidth = Math.max(50, fullLabel.length * 5 + 10);
|
||||
html += '<rect x="' + (labelX - rectWidth/2) + '" y="' + (labelY - 8) + '" width="' + rectWidth + '" height="12" rx="2" fill="#1e1e1e" opacity="0.9"/>';
|
||||
html += '<text x="' + labelX + '" y="' + (labelY + 2) + '" text-anchor="middle" fill="#ccc" font-size="9">';
|
||||
html += this.escapeHtml(conn.output_label);
|
||||
if (conn.output_location) {
|
||||
html += '<tspan font-style="italic" fill="#888"> · ' + this.escapeHtml(conn.output_location) + '</tspan>';
|
||||
}
|
||||
html += '</text>';
|
||||
}
|
||||
|
||||
|
|
@ -7416,6 +7422,21 @@
|
|||
|
||||
}
|
||||
|
||||
// Schutzgruppen-Markierung (FI/RCD)
|
||||
if (eq.fk_protection) {
|
||||
// Geschütztes Equipment: farbiger Balken unten (wie PWA)
|
||||
var protColor = self.getProtectionColor(eq.fk_protection);
|
||||
blockHtml += '<rect x="0" y="' + (blockHeight - 4) + '" width="' + blockWidth + '" height="4" ';
|
||||
blockHtml += 'fill="' + protColor + '" opacity="0.9" pointer-events="none"/>';
|
||||
}
|
||||
// Schutzgerät selbst: linker Balken
|
||||
var isProtectionDevice = self.equipment.some(function(e) { return e.fk_protection == eq.id; });
|
||||
if (isProtectionDevice) {
|
||||
var deviceColor = self.getProtectionColor(eq.id);
|
||||
blockHtml += '<rect x="-4" y="0" width="4" height="' + blockHeight + '" ';
|
||||
blockHtml += 'fill="' + deviceColor + '" rx="2" opacity="0.9"/>';
|
||||
}
|
||||
|
||||
blockHtml += '</g>';
|
||||
|
||||
// Terminals (bidirectional)
|
||||
|
|
@ -7790,7 +7811,8 @@
|
|||
var $layer = $(this.svgElement).find('.schematic-connections-layer');
|
||||
$layer.empty();
|
||||
|
||||
var html = '';
|
||||
var htmlBack = ''; // Leitungen (unten)
|
||||
var htmlFront = ''; // Abgänge + Eingänge (oben)
|
||||
var renderedCount = 0;
|
||||
|
||||
// Get display settings
|
||||
|
|
@ -7798,6 +7820,31 @@
|
|||
var shadowWidth = wireWidth + 4;
|
||||
var hoverWidth = wireWidth + 1.5;
|
||||
|
||||
// Abgang-Bereiche vorab berechnen (für Badge-Kollisionserkennung)
|
||||
this._outputAreas = [];
|
||||
this.connections.forEach(function(conn) {
|
||||
if (!conn.fk_target && !conn.path_data && conn.fk_source) {
|
||||
var eq = self.getEquipmentById(conn.fk_source);
|
||||
if (!eq) return;
|
||||
var terms = self.getTerminals(eq);
|
||||
var termId = conn.source_terminal_id || 't2';
|
||||
var pos = self.getTerminalPosition(eq, termId, terms);
|
||||
if (!pos) return;
|
||||
var labelText = conn.output_label || '';
|
||||
if (conn.output_location) labelText += ' · ' + conn.output_location;
|
||||
var cableText = ((conn.medium_type || '') + ' ' + (conn.medium_spec || '')).trim();
|
||||
var maxLen = Math.max(labelText.length, cableText.length);
|
||||
var lineLen = Math.min(120, Math.max(50, maxLen * 6 + 20));
|
||||
var goUp = pos.isTop;
|
||||
self._outputAreas.push({
|
||||
x: pos.x - 25,
|
||||
y: goUp ? (pos.y - lineLen - 5) : pos.y,
|
||||
w: 50,
|
||||
h: lineLen + 10
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.connections.forEach(function(conn, connIndex) {
|
||||
// Check is_rail as integer (PHP may return string "1" or "0")
|
||||
if (parseInt(conn.is_rail) === 1) {
|
||||
|
|
@ -7854,6 +7901,7 @@
|
|||
|
||||
// Calculate line length based on label text
|
||||
var labelText = conn.output_label || '';
|
||||
if (conn.output_location) labelText += ' · ' + conn.output_location;
|
||||
var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || '');
|
||||
var maxTextLen = Math.max(labelText.length, cableText.trim().length);
|
||||
var lineLength = Math.min(120, Math.max(50, maxTextLen * 6 + 20));
|
||||
|
|
@ -7869,45 +7917,49 @@
|
|||
// Draw vertical line
|
||||
var path = 'M ' + lineX + ' ' + startY + ' L ' + lineX + ' ' + endY;
|
||||
|
||||
html += '<g class="schematic-output-group' + (isBundled ? ' bundled' : '') + '" data-connection-id="' + conn.id + '" style="cursor:pointer;">';
|
||||
// Abgänge in htmlFront (über Leitungen)
|
||||
htmlFront += '<g class="schematic-output-group' + (isBundled ? ' bundled' : '') + '" data-connection-id="' + conn.id + '" style="cursor:pointer;">';
|
||||
|
||||
// For bundled: draw horizontal bar connecting all terminals
|
||||
if (isBundled && bundleWidth > 0) {
|
||||
var barY = startY + (goingUp ? -5 : 5);
|
||||
html += '<line x1="' + (bundleCenterX - bundleWidth/2) + '" y1="' + barY + '" ';
|
||||
html += 'x2="' + (bundleCenterX + bundleWidth/2) + '" y2="' + barY + '" ';
|
||||
html += 'stroke="' + color + '" stroke-width="' + (wireWidth + 1) + '" stroke-linecap="round"/>';
|
||||
htmlFront += '<line x1="' + (bundleCenterX - bundleWidth/2) + '" y1="' + barY + '" ';
|
||||
htmlFront += 'x2="' + (bundleCenterX + bundleWidth/2) + '" y2="' + barY + '" ';
|
||||
htmlFront += 'stroke="' + color + '" stroke-width="' + (wireWidth + 1) + '" stroke-linecap="round"/>';
|
||||
}
|
||||
|
||||
// Invisible hit area for clicking
|
||||
var hitY = goingUp ? endY : startY;
|
||||
var hitWidth = isBundled && bundleWidth > 0 ? Math.max(40, bundleWidth + 20) : 40;
|
||||
html += '<rect x="' + (lineX - hitWidth/2) + '" y="' + hitY + '" width="' + hitWidth + '" height="' + lineLength + '" fill="transparent"/>';
|
||||
htmlFront += '<rect x="' + (lineX - hitWidth/2) + '" y="' + hitY + '" width="' + hitWidth + '" height="' + lineLength + '" fill="transparent"/>';
|
||||
|
||||
// Connection line
|
||||
html += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
|
||||
htmlFront += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
htmlFront += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
|
||||
|
||||
// Arrow at end (pointing away from equipment)
|
||||
var arrowSize = isBundled && bundleWidth > 0 ? 8 : 5;
|
||||
if (goingUp) {
|
||||
// Arrow pointing UP
|
||||
html += '<polygon points="' + (lineX - arrowSize) + ',' + (endY + 6) + ' ' + lineX + ',' + endY + ' ' + (lineX + arrowSize) + ',' + (endY + 6) + '" fill="' + color + '"/>';
|
||||
htmlFront += '<polygon points="' + (lineX - arrowSize) + ',' + (endY + 6) + ' ' + lineX + ',' + endY + ' ' + (lineX + arrowSize) + ',' + (endY + 6) + '" fill="' + color + '"/>';
|
||||
} else {
|
||||
// Arrow pointing DOWN
|
||||
html += '<polygon points="' + (lineX - arrowSize) + ',' + (endY - 6) + ' ' + lineX + ',' + endY + ' ' + (lineX + arrowSize) + ',' + (endY - 6) + '" fill="' + color + '"/>';
|
||||
htmlFront += '<polygon points="' + (lineX - arrowSize) + ',' + (endY - 6) + ' ' + lineX + ',' + endY + ' ' + (lineX + arrowSize) + ',' + (endY - 6) + '" fill="' + color + '"/>';
|
||||
}
|
||||
|
||||
// Labels - vertical text on both sides
|
||||
var labelY = (startY + endY) / 2;
|
||||
|
||||
// Left side: Bezeichnung (output_label)
|
||||
// Left side: Bezeichnung (output_label) + Räumlichkeit
|
||||
if (conn.output_label) {
|
||||
html += '<text x="' + (lineX - 10) + '" y="' + labelY + '" ';
|
||||
html += 'text-anchor="middle" fill="#fff" font-size="11" font-weight="bold" ';
|
||||
html += 'transform="rotate(-90 ' + (lineX - 10) + ' ' + labelY + ')">';
|
||||
html += self.escapeHtml(conn.output_label);
|
||||
html += '</text>';
|
||||
htmlFront += '<text x="' + (lineX - 10) + '" y="' + labelY + '" ';
|
||||
htmlFront += 'text-anchor="middle" fill="#fff" font-size="11" font-weight="bold" ';
|
||||
htmlFront += 'transform="rotate(-90 ' + (lineX - 10) + ' ' + labelY + ')">';
|
||||
htmlFront += self.escapeHtml(conn.output_label);
|
||||
if (conn.output_location) {
|
||||
htmlFront += '<tspan font-size="9" font-weight="normal" font-style="italic" fill="#999"> · ' + self.escapeHtml(conn.output_location) + '</tspan>';
|
||||
}
|
||||
htmlFront += '</text>';
|
||||
}
|
||||
|
||||
// Right side: Kabeltyp + Größe
|
||||
|
|
@ -7915,23 +7967,23 @@
|
|||
if (conn.medium_type) cableInfo = conn.medium_type;
|
||||
if (conn.medium_spec) cableInfo += ' ' + conn.medium_spec;
|
||||
if (cableInfo) {
|
||||
html += '<text x="' + (lineX + 10) + '" y="' + labelY + '" ';
|
||||
html += 'text-anchor="middle" fill="#888" font-size="10" ';
|
||||
html += 'transform="rotate(-90 ' + (lineX + 10) + ' ' + labelY + ')">';
|
||||
html += self.escapeHtml(cableInfo.trim());
|
||||
html += '</text>';
|
||||
htmlFront += '<text x="' + (lineX + 10) + '" y="' + labelY + '" ';
|
||||
htmlFront += 'text-anchor="middle" fill="#888" font-size="10" ';
|
||||
htmlFront += 'transform="rotate(-90 ' + (lineX + 10) + ' ' + labelY + ')">';
|
||||
htmlFront += self.escapeHtml(cableInfo.trim());
|
||||
htmlFront += '</text>';
|
||||
}
|
||||
|
||||
// Phase type at end of line
|
||||
if (conn.connection_type) {
|
||||
var phaseY = goingUp ? (endY - 10) : (endY + 14);
|
||||
html += '<text x="' + lineX + '" y="' + phaseY + '" ';
|
||||
html += 'text-anchor="middle" fill="' + color + '" font-size="11" font-weight="bold">';
|
||||
html += conn.connection_type;
|
||||
html += '</text>';
|
||||
htmlFront += '<text x="' + lineX + '" y="' + phaseY + '" ';
|
||||
htmlFront += 'text-anchor="middle" fill="' + color + '" font-size="11" font-weight="bold">';
|
||||
htmlFront += conn.connection_type;
|
||||
htmlFront += '</text>';
|
||||
}
|
||||
|
||||
html += '</g>';
|
||||
htmlFront += '</g>';
|
||||
renderedCount++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -7957,18 +8009,18 @@
|
|||
var junctionX = pathMatch ? parseFloat(pathMatch[1]) : 0;
|
||||
var junctionY = pathMatch ? parseFloat(pathMatch[2]) : 0;
|
||||
|
||||
html += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
htmlBack += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
|
||||
html += '<g class="schematic-junction-group" data-connection-id="' + conn.id + '">';
|
||||
html += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
htmlBack += '<g class="schematic-junction-group" data-connection-id="' + conn.id + '">';
|
||||
htmlBack += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
|
||||
html += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="' + junctionColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
htmlBack += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="' + junctionColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
|
||||
// Junction marker (dot at start point)
|
||||
if (self.displaySettings.showJunctions) {
|
||||
html += '<circle cx="' + junctionX + '" cy="' + junctionY + '" r="5" fill="' + junctionColor + '" stroke="#fff" stroke-width="1.5"/>';
|
||||
htmlBack += '<circle cx="' + junctionX + '" cy="' + junctionY + '" r="5" fill="' + junctionColor + '" stroke="#fff" stroke-width="1.5"/>';
|
||||
}
|
||||
|
||||
// Label if present
|
||||
|
|
@ -7983,15 +8035,15 @@
|
|||
|
||||
// Badge with solid background and border for visibility
|
||||
// Text always white for readability (e.g. L2 black wire)
|
||||
html += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
html += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
html += 'fill="#1a1a1a" stroke="' + junctionColor + '" stroke-width="1.5"/>';
|
||||
html += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
html += self.escapeHtml(conn.output_label);
|
||||
html += '</text>';
|
||||
htmlBack += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
htmlBack += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
htmlBack += 'fill="#1a1a1a" stroke="' + junctionColor + '" stroke-width="1.5"/>';
|
||||
htmlBack += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
htmlBack += self.escapeHtml(conn.output_label);
|
||||
htmlBack += '</text>';
|
||||
}
|
||||
|
||||
html += '</g>';
|
||||
htmlBack += '</g>';
|
||||
renderedCount++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -8009,63 +8061,57 @@
|
|||
|
||||
// Farbe: gespeichert > Phase-Farbe > Fallback hellblau
|
||||
var inputColor = conn.color || self.PHASE_COLORS[conn.connection_type] || '#4fc3f7';
|
||||
var isTop = targetPos.isTop;
|
||||
|
||||
// Calculate line length based on label
|
||||
// Linienlänge basierend auf Label
|
||||
var inputLabel = conn.output_label || '';
|
||||
var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30));
|
||||
var startY = targetPos.y - inputLineLength;
|
||||
|
||||
// Draw vertical line coming down into terminal
|
||||
// Richtung: Top-Terminal = Linie von oben, Bottom-Terminal = Linie von unten
|
||||
var startY = isTop ? (targetPos.y - inputLineLength) : (targetPos.y + inputLineLength);
|
||||
|
||||
var path = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y;
|
||||
|
||||
html += '<g class="schematic-input-group" data-connection-id="' + conn.id + '" style="cursor:pointer;">';
|
||||
// Eingänge in htmlFront (über Leitungen)
|
||||
htmlFront += '<g class="schematic-input-group" data-connection-id="' + conn.id + '" style="cursor:pointer;">';
|
||||
|
||||
// Invisible hit area for clicking
|
||||
html += '<rect x="' + (targetPos.x - 20) + '" y="' + startY + '" width="40" height="' + inputLineLength + '" fill="transparent"/>';
|
||||
// Invisible hit area
|
||||
var hitY = isTop ? startY : targetPos.y;
|
||||
htmlFront += '<rect x="' + (targetPos.x - 20) + '" y="' + hitY + '" width="40" height="' + inputLineLength + '" fill="transparent"/>';
|
||||
|
||||
// Connection line
|
||||
html += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="' + inputColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
|
||||
// Verbindungslinie
|
||||
htmlFront += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
htmlFront += 'fill="none" stroke="' + inputColor + '" stroke-width="' + wireWidth + '" stroke-linecap="round"/>';
|
||||
|
||||
// Circle at top (external source indicator)
|
||||
html += '<circle cx="' + targetPos.x + '" cy="' + startY + '" r="6" fill="' + inputColor + '" stroke="#fff" stroke-width="2"/>';
|
||||
// Kreis am externen Ende (Quell-Indikator)
|
||||
htmlFront += '<circle cx="' + targetPos.x + '" cy="' + startY + '" r="6" fill="' + inputColor + '" stroke="#fff" stroke-width="2"/>';
|
||||
|
||||
// Arrow pointing down into terminal
|
||||
html += '<polygon points="' + (targetPos.x - 5) + ',' + (targetPos.y - 8) + ' ' + targetPos.x + ',' + (targetPos.y - 2) + ' ' + (targetPos.x + 5) + ',' + (targetPos.y - 8) + '" fill="' + inputColor + '"/>';
|
||||
|
||||
// Phase-Label als Badge über dem Eingang
|
||||
var phaseLabel = conn.connection_type || 'L1';
|
||||
var phaseBadgeWidth = Math.max(phaseLabel.length * 9 + 12, 30);
|
||||
var phaseBadgeHeight = 22;
|
||||
var phaseBadgeX = targetPos.x - phaseBadgeWidth / 2;
|
||||
var phaseBadgeY = startY - phaseBadgeHeight - 8;
|
||||
|
||||
html += '<rect x="' + phaseBadgeX + '" y="' + phaseBadgeY + '" ';
|
||||
html += 'width="' + phaseBadgeWidth + '" height="' + phaseBadgeHeight + '" rx="4" ';
|
||||
html += 'fill="' + inputColor + '" stroke="#fff" stroke-width="1"/>';
|
||||
html += '<text x="' + targetPos.x + '" y="' + (phaseBadgeY + 16) + '" ';
|
||||
html += 'text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
html += phaseLabel;
|
||||
html += '</text>';
|
||||
|
||||
// Bezeichnung als Badge neben der Eingangsleitung
|
||||
if (conn.output_label) {
|
||||
var badgeText = self.escapeHtml(conn.output_label);
|
||||
var badgeWidth = Math.min(badgeText.length * 7 + 16, 140);
|
||||
var badgeHeight = 20;
|
||||
var badgeX = targetPos.x + 14;
|
||||
var badgeY = startY + (inputLineLength / 2) - badgeHeight / 2;
|
||||
|
||||
html += '<rect x="' + badgeX + '" y="' + badgeY + '" ';
|
||||
html += 'width="' + badgeWidth + '" height="' + badgeHeight + '" rx="4" ';
|
||||
html += 'fill="#1a1a1a" stroke="' + inputColor + '" stroke-width="1.5"/>';
|
||||
html += '<text x="' + (badgeX + badgeWidth / 2) + '" y="' + (badgeY + 14) + '" ';
|
||||
html += 'text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">';
|
||||
html += badgeText;
|
||||
html += '</text>';
|
||||
// Pfeil ins Terminal (Richtung abhängig von Position)
|
||||
if (isTop) {
|
||||
// Pfeil nach unten ins Top-Terminal
|
||||
htmlFront += '<polygon points="' + (targetPos.x - 5) + ',' + (targetPos.y - 8) + ' ' + targetPos.x + ',' + (targetPos.y - 2) + ' ' + (targetPos.x + 5) + ',' + (targetPos.y - 8) + '" fill="' + inputColor + '"/>';
|
||||
} else {
|
||||
// Pfeil nach oben ins Bottom-Terminal
|
||||
htmlFront += '<polygon points="' + (targetPos.x - 5) + ',' + (targetPos.y + 8) + ' ' + targetPos.x + ',' + (targetPos.y + 2) + ' ' + (targetPos.x + 5) + ',' + (targetPos.y + 8) + '" fill="' + inputColor + '"/>';
|
||||
}
|
||||
|
||||
html += '</g>';
|
||||
// Badge am Ende der Linie: Bezeichnung wenn vorhanden, sonst Phase
|
||||
var badgeLabel = conn.output_label ? self.escapeHtml(conn.output_label) : (conn.connection_type || 'L1');
|
||||
var phaseBadgeWidth = Math.max(badgeLabel.length * 9 + 12, 30);
|
||||
var phaseBadgeHeight = 22;
|
||||
var phaseBadgeX = targetPos.x - phaseBadgeWidth / 2;
|
||||
var phaseBadgeY = isTop ? (startY - phaseBadgeHeight - 8) : (startY + 8);
|
||||
|
||||
htmlFront += '<rect x="' + phaseBadgeX + '" y="' + phaseBadgeY + '" ';
|
||||
htmlFront += 'width="' + phaseBadgeWidth + '" height="' + phaseBadgeHeight + '" rx="4" ';
|
||||
htmlFront += 'fill="' + inputColor + '" stroke="#fff" stroke-width="1"/>';
|
||||
htmlFront += '<text x="' + targetPos.x + '" y="' + (phaseBadgeY + 16) + '" ';
|
||||
htmlFront += 'text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
htmlFront += badgeLabel;
|
||||
htmlFront += '</text>';
|
||||
|
||||
|
||||
htmlFront += '</g>';
|
||||
renderedCount++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -8084,17 +8130,17 @@
|
|||
var junctionX = lastMatch ? parseFloat(lastMatch[1]) : 0;
|
||||
var junctionY = lastMatch ? parseFloat(lastMatch[2]) : 0;
|
||||
|
||||
html += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
htmlBack += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
|
||||
html += '<g class="schematic-connection-group" data-connection-id="' + conn.id + '">';
|
||||
html += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
htmlBack += '<g class="schematic-connection-group" data-connection-id="' + conn.id + '">';
|
||||
htmlBack += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
|
||||
html += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
htmlBack += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
|
||||
// Junction marker (dot at end point where it connects to other wire)
|
||||
html += '<circle cx="' + junctionX + '" cy="' + junctionY + '" r="4" fill="' + color + '" stroke="#fff" stroke-width="1.5"/>';
|
||||
htmlBack += '<circle cx="' + junctionX + '" cy="' + junctionY + '" r="4" fill="' + color + '" stroke="#fff" stroke-width="1.5"/>';
|
||||
|
||||
// Label if present
|
||||
if (conn.output_label) {
|
||||
|
|
@ -8104,15 +8150,15 @@
|
|||
var labelX = labelPos ? labelPos.x : junctionX;
|
||||
var labelY = labelPos ? labelPos.y : junctionY - 20;
|
||||
|
||||
html += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
html += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
html += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1.5"/>';
|
||||
html += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
html += self.escapeHtml(conn.output_label);
|
||||
html += '</text>';
|
||||
htmlBack += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
htmlBack += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
htmlBack += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1.5"/>';
|
||||
htmlBack += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
htmlBack += self.escapeHtml(conn.output_label);
|
||||
htmlBack += '</text>';
|
||||
}
|
||||
|
||||
html += '</g>';
|
||||
htmlBack += '</g>';
|
||||
renderedCount++;
|
||||
return;
|
||||
}
|
||||
|
|
@ -8144,21 +8190,18 @@
|
|||
path = self.createOrthogonalPath(sourcePos, targetPos, routeOffset, sourceEq, targetEq);
|
||||
}
|
||||
|
||||
html += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
htmlBack += '<path class="schematic-connection-shadow" d="' + path + '" fill="none" stroke="rgba(0,0,0,0.4)" stroke-width="' + shadowWidth + '" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
|
||||
html += '<g class="schematic-connection-group" data-connection-id="' + conn.id + '">';
|
||||
html += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
htmlBack += '<g class="schematic-connection-group" data-connection-id="' + conn.id + '">';
|
||||
htmlBack += '<path class="schematic-connection-hitarea" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="transparent" stroke-width="15" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;"/>';
|
||||
|
||||
html += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
html += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
htmlBack += '<path class="schematic-connection" data-connection-id="' + conn.id + '" d="' + path + '" ';
|
||||
htmlBack += 'fill="none" stroke="' + color + '" stroke-width="' + wireWidth + '" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"/>';
|
||||
|
||||
if (conn.output_label) {
|
||||
// Calculate label dimensions
|
||||
var labelWidth = Math.min(conn.output_label.length * 8 + 16, 120);
|
||||
var labelHeight = 22;
|
||||
|
||||
// Find safe label position that doesn't overlap equipment
|
||||
var labelPos = self.findSafeLabelPosition(path, labelWidth, labelHeight);
|
||||
if (!labelPos) {
|
||||
labelPos = self.getPathMidpoint(path);
|
||||
|
|
@ -8166,39 +8209,35 @@
|
|||
var labelX = labelPos ? labelPos.x : (sourcePos.x + targetPos.x) / 2;
|
||||
var labelY = labelPos ? labelPos.y : (sourcePos.y + targetPos.y) / 2;
|
||||
|
||||
// Badge with solid background and border for visibility
|
||||
// Text always white for readability (e.g. L2 black wire)
|
||||
html += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
html += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
html += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1.5"/>';
|
||||
html += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
html += self.escapeHtml(conn.output_label);
|
||||
html += '</text>';
|
||||
htmlBack += '<rect class="connection-label-bg" x="' + (labelX - labelWidth/2) + '" y="' + (labelY - 12) + '" ';
|
||||
htmlBack += 'width="' + labelWidth + '" height="' + labelHeight + '" rx="4" ';
|
||||
htmlBack += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1.5"/>';
|
||||
htmlBack += '<text x="' + labelX + '" y="' + (labelY + 4) + '" text-anchor="middle" fill="#fff" font-size="13" font-weight="bold">';
|
||||
htmlBack += self.escapeHtml(conn.output_label);
|
||||
htmlBack += '</text>';
|
||||
}
|
||||
|
||||
if (conn.connection_type && !conn.output_label) {
|
||||
// Also check for safe position for type labels
|
||||
var typeWidth = conn.connection_type.length * 9 + 14;
|
||||
var typeHeight = 18;
|
||||
var typePos = self.findSafeLabelPosition(path, typeWidth, typeHeight);
|
||||
var typeX = typePos ? typePos.x : (sourcePos.x + targetPos.x) / 2;
|
||||
var typeY = typePos ? typePos.y : (sourcePos.y + targetPos.y) / 2;
|
||||
|
||||
// Badge background for type label too
|
||||
// Text always white for readability
|
||||
html += '<rect class="connection-type-bg" x="' + (typeX - typeWidth/2) + '" y="' + (typeY - 10) + '" ';
|
||||
html += 'width="' + typeWidth + '" height="' + typeHeight + '" rx="3" ';
|
||||
html += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1"/>';
|
||||
html += '<text x="' + typeX + '" y="' + (typeY + 4) + '" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">';
|
||||
html += conn.connection_type;
|
||||
html += '</text>';
|
||||
htmlBack += '<rect class="connection-type-bg" x="' + (typeX - typeWidth/2) + '" y="' + (typeY - 10) + '" ';
|
||||
htmlBack += 'width="' + typeWidth + '" height="' + typeHeight + '" rx="3" ';
|
||||
htmlBack += 'fill="#1a1a1a" stroke="' + color + '" stroke-width="1"/>';
|
||||
htmlBack += '<text x="' + typeX + '" y="' + (typeY + 4) + '" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">';
|
||||
htmlBack += conn.connection_type;
|
||||
htmlBack += '</text>';
|
||||
}
|
||||
|
||||
html += '</g>';
|
||||
htmlBack += '</g>';
|
||||
renderedCount++;
|
||||
});
|
||||
|
||||
$layer.html(html);
|
||||
// Leitungen zuerst (hinten), dann Abgänge/Eingänge (vorne)
|
||||
$layer.html(htmlBack + htmlFront);
|
||||
|
||||
// Bind click events to SVG connection elements (must be done after rendering)
|
||||
var self = this;
|
||||
|
|
@ -9686,6 +9725,17 @@
|
|||
return result.top;
|
||||
},
|
||||
|
||||
// Schutzgruppen-Farbe basierend auf Protection-Device-ID
|
||||
_protectionColorCache: {},
|
||||
getProtectionColor: function(protectionId) {
|
||||
if (!protectionId) return null;
|
||||
if (this._protectionColorCache[protectionId]) return this._protectionColorCache[protectionId];
|
||||
var colors = ['#e74c3c', '#3498db', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4', '#ff5722'];
|
||||
var idx = Object.keys(this._protectionColorCache).length % colors.length;
|
||||
this._protectionColorCache[protectionId] = colors[idx];
|
||||
return colors[idx];
|
||||
},
|
||||
|
||||
getTerminals: function(eq) {
|
||||
// Try to parse terminals_config from equipment type
|
||||
if (eq.terminals_config) {
|
||||
|
|
@ -10009,6 +10059,17 @@
|
|||
return eq; // Return the overlapping equipment
|
||||
}
|
||||
}
|
||||
|
||||
// Auch gegen Abgang-Bereiche prüfen
|
||||
if (this._outputAreas) {
|
||||
for (var j = 0; j < this._outputAreas.length; j++) {
|
||||
var oa = this._outputAreas[j];
|
||||
if (!(lx2 < oa.x || lx1 > oa.x + oa.w || ly2 < oa.y || ly1 > oa.y + oa.h)) {
|
||||
return { _isOutput: true }; // Abgang-Bereich überlappt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No overlap
|
||||
},
|
||||
|
||||
|
|
@ -10031,7 +10092,7 @@
|
|||
|
||||
if (points.length < 2) return null;
|
||||
|
||||
// Try positions along the path (at 10%, 30%, 50%, 70%, 90%)
|
||||
// Positionen entlang des Pfades testen (Mitte bevorzugt, dann alternierend)
|
||||
var percentages = [0.5, 0.3, 0.7, 0.2, 0.8, 0.1, 0.9];
|
||||
|
||||
// Calculate total path length and segment lengths
|
||||
|
|
@ -11307,6 +11368,7 @@
|
|||
}
|
||||
|
||||
var labelText = conn.output_label || '';
|
||||
if (conn.output_location) labelText += ' · ' + conn.output_location;
|
||||
var cableText = (conn.medium_type || '') + ' ' + (conn.medium_spec || '');
|
||||
var maxTextLen = Math.max(labelText.length, cableText.trim().length);
|
||||
var lineLength = Math.min(120, Math.max(50, maxTextLen * 6 + 20));
|
||||
|
|
@ -11314,7 +11376,7 @@
|
|||
var endY = goingUp ? (sourcePos.y - lineLength) : (sourcePos.y + lineLength);
|
||||
pathData = 'M ' + lineX + ' ' + sourcePos.y + ' L ' + lineX + ' ' + endY;
|
||||
} else if (!conn.fk_source && targetEq) {
|
||||
// Anschlusspunkt (Input) - Linie von oben ins Terminal
|
||||
// Anschlusspunkt (Input) - Linie von außen ins Terminal
|
||||
var targetTerminals = this.getTerminals(targetEq);
|
||||
var targetTermId = conn.target_terminal_id || 't1';
|
||||
var targetPos = this.getTerminalPosition(targetEq, targetTermId, targetTerminals);
|
||||
|
|
@ -11322,7 +11384,7 @@
|
|||
|
||||
var inputLabel = conn.output_label || '';
|
||||
var inputLineLength = Math.min(80, Math.max(45, inputLabel.length * 5 + 30));
|
||||
var startY = targetPos.y - inputLineLength;
|
||||
var startY = targetPos.isTop ? (targetPos.y - inputLineLength) : (targetPos.y + inputLineLength);
|
||||
pathData = 'M ' + targetPos.x + ' ' + startY + ' L ' + targetPos.x + ' ' + targetPos.y;
|
||||
}
|
||||
|
||||
|
|
@ -12525,6 +12587,14 @@
|
|||
'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;">';
|
||||
html += '</div>';
|
||||
|
||||
// Räumlichkeit
|
||||
html += '<div style="margin-bottom:10px;">';
|
||||
html += '<label style="display:block;color:#aaa;font-size:11px;margin-bottom:3px;">Räumlichkeit:</label>';
|
||||
html += '<input type="text" class="output-location" placeholder="z.B. Küche, Bad OG, Keller" value="' +
|
||||
self.escapeHtml(existingOutput ? existingOutput.output_location || '' : '') + '" ' +
|
||||
'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;">';
|
||||
html += '</div>';
|
||||
|
||||
// Kabeltyp (from database)
|
||||
html += '<div style="margin-bottom:10px;">';
|
||||
html += '<label style="display:block;color:#aaa;font-size:11px;margin-bottom:3px;">Kabeltyp:</label>';
|
||||
|
|
@ -12577,6 +12647,23 @@
|
|||
});
|
||||
html += '</select></div>';
|
||||
|
||||
// Alle Terminals auf dieser Seite bündeln (nur bei >1 Terminal auf gleicher Seite)
|
||||
var eq = self.equipment.find(function(e) { return e.id == eqId; });
|
||||
var terminals = eq ? self.getTerminals(eq) : [];
|
||||
// Terminal-Seite ermitteln (top oder bottom)
|
||||
var clickedTerm = terminals.find(function(t) { return t.id === termId; });
|
||||
var termSide = clickedTerm ? clickedTerm.pos : 'bottom';
|
||||
var sideTerminals = terminals.filter(function(t) { return t.pos === termSide; });
|
||||
var sideCount = sideTerminals.length;
|
||||
if (sideCount > 1) {
|
||||
html += '<div style="margin-bottom:12px;">';
|
||||
html += '<label style="display:flex;align-items:center;gap:8px;color:#ccc;cursor:pointer;">';
|
||||
html += '<input type="checkbox" class="output-bundle-all"' +
|
||||
(existingOutput && existingOutput.bundled_terminals === 'all' ? ' checked' : '') + '>';
|
||||
html += '<span>Alle ' + sideCount + ' Klemmen bündeln (Drehstrom-Verbraucher)</span>';
|
||||
html += '</label></div>';
|
||||
}
|
||||
|
||||
// Buttons
|
||||
html += '<div style="display:flex;gap:8px;justify-content:flex-end;">';
|
||||
if (existingOutput) {
|
||||
|
|
@ -12628,15 +12715,17 @@
|
|||
});
|
||||
$('.output-save-btn').on('click', function() {
|
||||
var label = $('.output-label').val();
|
||||
var location = $('.output-location').val();
|
||||
var cableType = $('.output-cable-type').val();
|
||||
var cableSpec = $('.output-cable-spec').val();
|
||||
var cableLength = $('.output-length').val();
|
||||
var phaseType = $('.output-phase-type').val();
|
||||
var bundled = $('.output-bundle-all').is(':checked') ? 'all' : '';
|
||||
|
||||
if (existingOutput) {
|
||||
self.updateOutput(existingOutput.id, label, cableType, cableSpec, phaseType, cableLength);
|
||||
self.updateOutput(existingOutput.id, label, location, cableType, cableSpec, phaseType, cableLength, bundled);
|
||||
} else {
|
||||
self.createOutput(eqId, termId, label, cableType, cableSpec, phaseType, cableLength);
|
||||
self.createOutput(eqId, termId, label, location, cableType, cableSpec, phaseType, cableLength, bundled);
|
||||
}
|
||||
$('.schematic-output-dialog').remove();
|
||||
});
|
||||
|
|
@ -12711,7 +12800,7 @@
|
|||
},
|
||||
|
||||
// Create a new cable output (no target, fk_target = NULL)
|
||||
createOutput: function(eqId, termId, label, cableType, cableSpec, phaseType, cableLength) {
|
||||
createOutput: function(eqId, termId, label, location, cableType, cableSpec, phaseType, cableLength, bundledTerminals) {
|
||||
var self = this;
|
||||
|
||||
$.ajax({
|
||||
|
|
@ -12725,9 +12814,11 @@
|
|||
target_terminal_id: '',
|
||||
connection_type: phaseType || 'L1N',
|
||||
output_label: label,
|
||||
output_location: location || '',
|
||||
medium_type: cableType,
|
||||
medium_spec: cableSpec,
|
||||
medium_length: cableLength || '',
|
||||
bundled_terminals: bundledTerminals || '',
|
||||
token: $('input[name="token"]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
|
|
@ -12746,7 +12837,7 @@
|
|||
},
|
||||
|
||||
// Update existing output
|
||||
updateOutput: function(connId, label, cableType, cableSpec, phaseType, cableLength) {
|
||||
updateOutput: function(connId, label, location, cableType, cableSpec, phaseType, cableLength, bundledTerminals) {
|
||||
var self = this;
|
||||
|
||||
$.ajax({
|
||||
|
|
@ -12757,9 +12848,11 @@
|
|||
connection_id: connId,
|
||||
connection_type: phaseType || 'L1N',
|
||||
output_label: label,
|
||||
output_location: location || '',
|
||||
medium_type: cableType,
|
||||
medium_spec: cableSpec,
|
||||
medium_length: cableLength || '',
|
||||
bundled_terminals: bundledTerminals || '',
|
||||
token: $('input[name="token"]').val()
|
||||
},
|
||||
dataType: 'json',
|
||||
|
|
@ -12992,7 +13085,23 @@
|
|||
var conn = this.connections.find(function(c) { return c.id == connId; });
|
||||
if (!conn) return;
|
||||
|
||||
// Build edit dialog
|
||||
// Abgang → showAbgangDialog mit existingOutput
|
||||
if (conn.fk_source && !conn.fk_target && !conn.path_data) {
|
||||
var eqId = conn.fk_source;
|
||||
var termId = conn.source_terminal_id || 't2';
|
||||
this.showAbgangDialog(eqId, termId, window.innerWidth / 2 - 160, window.innerHeight / 2 - 150, conn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Eingang → showInputDialog mit existingInput
|
||||
if (!conn.fk_source && conn.fk_target && !conn.path_data) {
|
||||
var eqId = conn.fk_target;
|
||||
var termId = conn.target_terminal_id || 't1';
|
||||
this.showInputDialog(eqId, termId, window.innerWidth / 2 - 140, window.innerHeight / 2 - 100, conn);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normale Verbindung → Standard-Edit-Dialog
|
||||
var dialogHtml = '<div class="schematic-edit-dialog" style="' +
|
||||
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' +
|
||||
'background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;' +
|
||||
|
|
@ -14242,6 +14351,13 @@
|
|||
dialogHtml += '</div>';
|
||||
dialogHtml += '</div>';
|
||||
|
||||
// FI/RCD-Zuordnung
|
||||
dialogHtml += '<div style="margin-bottom:12px;padding-top:12px;border-top:1px solid #444;">';
|
||||
dialogHtml += '<label style="display:block;color:#aaa;font-size:12px;margin-bottom:4px;"><i class="fa fa-shield" style="color:#e67e22;"></i> Schutzgerät (FI/RCD):</label>';
|
||||
dialogHtml += '<select class="edit-equipment-protection" style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;">';
|
||||
dialogHtml += '<option value="">-- Keins --</option>';
|
||||
dialogHtml += '</select></div>';
|
||||
|
||||
// Buttons
|
||||
dialogHtml += '<div style="display:flex;gap:10px;justify-content:flex-end;">';
|
||||
dialogHtml += '<button type="button" class="edit-dialog-cancel" style="' +
|
||||
|
|
@ -14287,6 +14403,22 @@
|
|||
|
||||
// Produktsuche mit Autocomplete initialisieren
|
||||
self.initProductAutocomplete('.edit-product-search', '.edit-product-id', '.edit-product-clear', eq.fk_product);
|
||||
|
||||
// Protection Devices (FI/RCD) laden
|
||||
$.ajax({
|
||||
url: baseUrl + '/custom/kundenkarte/ajax/equipment.php',
|
||||
data: { action: 'get_protection_devices', anlage_id: self.anlageId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success && response.devices) {
|
||||
var $select = $('.edit-equipment-protection');
|
||||
response.devices.forEach(function(device) {
|
||||
var selected = (eq.fk_protection && parseInt(device.id) === parseInt(eq.fk_protection)) ? ' selected' : '';
|
||||
$select.append('<option value="' + device.id + '"' + selected + '>' + self.escapeHtml(device.display_label) + '</option>');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
saveEquipmentEdit: function(equipmentId) {
|
||||
|
|
@ -14309,6 +14441,7 @@
|
|||
position_te: $('.edit-equipment-position').val(),
|
||||
field_values: JSON.stringify(fieldValues),
|
||||
fk_product: $('.edit-product-id').val() || 0,
|
||||
fk_protection: $('.edit-equipment-protection').val() || 0,
|
||||
token: $('input[name="token"]').val()
|
||||
};
|
||||
|
||||
|
|
|
|||
341
js/pwa.js
341
js/pwa.js
|
|
@ -59,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));
|
||||
}
|
||||
|
||||
|
|
@ -749,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
|
||||
|
|
@ -760,6 +765,7 @@
|
|||
outputs: App.outputs,
|
||||
inputs: App.inputs,
|
||||
connections: App.connections,
|
||||
busbars: App.busbars,
|
||||
fieldMeta: App.fieldMeta
|
||||
}));
|
||||
|
||||
|
|
@ -780,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');
|
||||
|
|
@ -827,6 +834,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Terminal-Farbpropagierung aufbauen (Phasenfarben an allen Terminals)
|
||||
buildTerminalPhaseMap();
|
||||
|
||||
let html = '';
|
||||
|
||||
App.panels.forEach(panel => {
|
||||
|
|
@ -879,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) || 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>`;
|
||||
|
|
@ -914,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) || 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>`;
|
||||
|
|
@ -958,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 : ''}`;
|
||||
|
|
@ -1033,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) || 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>`;
|
||||
|
|
@ -1077,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 : ''}`;
|
||||
|
|
@ -1113,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) || 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>`;
|
||||
|
|
@ -2366,6 +2414,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
|
||||
|
|
@ -2388,7 +2437,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
|
||||
|
|
@ -2470,6 +2520,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
|
||||
*/
|
||||
|
|
@ -2709,6 +2953,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
|
||||
|
|
@ -2721,7 +2966,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
|
||||
|
|
@ -2745,6 +2991,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() || '') : '';
|
||||
|
|
@ -2773,6 +3020,7 @@
|
|||
connection_type: connectionType,
|
||||
color: color,
|
||||
output_label: outputLabel,
|
||||
output_location: outputLocation,
|
||||
medium_type: mediumType,
|
||||
medium_spec: mediumSpec,
|
||||
medium_length: mediumLength,
|
||||
|
|
@ -2786,6 +3034,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;
|
||||
|
|
@ -2839,6 +3088,7 @@
|
|||
connection_type: connectionType,
|
||||
color: color,
|
||||
output_label: outputLabel,
|
||||
output_location: outputLocation,
|
||||
medium_type: mediumType,
|
||||
medium_spec: mediumSpec,
|
||||
medium_length: mediumLength,
|
||||
|
|
@ -2852,6 +3102,7 @@
|
|||
connection_type: connectionType,
|
||||
color: color,
|
||||
output_label: outputLabel,
|
||||
output_location: outputLocation,
|
||||
medium_type: mediumType,
|
||||
medium_spec: mediumSpec,
|
||||
medium_length: mediumLength,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
0
manifest.json
Normal file → Executable file
0
manifest.json
Normal file → Executable file
37
pwa.php
Normal file → Executable file
37
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">
|
||||
|
|
@ -246,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>
|
||||
|
|
@ -377,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.8"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
0
pwa_auth.php
Normal file → Executable file
0
pwa_auth.php
Normal file → Executable file
4
sw.js
Normal file → Executable file
4
sw.js
Normal file → Executable file
|
|
@ -3,8 +3,8 @@
|
|||
* Offline-First für Schaltschrank-Dokumentation
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v11.7';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v11.7';
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v12.4';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v12.4';
|
||||
|
||||
// Statische Assets die immer gecached werden (ohne Query-String)
|
||||
const STATIC_ASSETS = [
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@ if (empty($customerSystems)) {
|
|||
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 !== '') {
|
||||
|
|
|
|||
|
|
@ -613,7 +613,7 @@ if (empty($customerSystems)) {
|
|||
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 !== '') {
|
||||
|
|
|
|||
2
werkzeuge.php
Normal file → Executable file
2
werkzeuge.php
Normal file → Executable file
|
|
@ -380,7 +380,7 @@ if (in_array($action, array('create', 'edit', 'view'))) {
|
|||
$typeFieldsList = $type->fetchFields();
|
||||
foreach ($typeFieldsList as $field) {
|
||||
if ($field->field_type === '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 !== '') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue