From 619d14e8d5479810c9b572995d56e60cc00ca178 Mon Sep 17 00:00:00 2001 From: data Date: Mon, 2 Mar 2026 14:34:54 +0100 Subject: [PATCH] =?UTF-8?q?feat(pwa):=20FI-Schutzgruppen,=20geb=C3=BCndelt?= =?UTF-8?q?e=20Terminals,=20Terminal-Konfiguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schutzgruppen-Zuordnung: Equipment kann FI/RCD zugeordnet werden - Farbliche Markierung der Schutzgruppen im Schaltplan - Dropdown zur Auswahl des Schutzgeräts im Equipment-Dialog - Gebündelte Terminals: Multi-Phasen-Abgänge (E-Herd, Durchlauferhitzer) - "Alle bündeln" Option im Abgang-Dialog - Zentriertes Label über alle Terminals des Equipment - Terminal-Anzahl aus terminals_config statt TE-Breite - Neozed 3F zeigt korrekt 3 statt 4 Terminals - Neue getTerminalCount() Hilfsfunktion - Zuletzt bearbeitete Kunden (max. 5) auf Search-Screen - Medium-Typen dynamisch aus DB mit Spezifikationen-Dropdown - Terminal-Labels anklickbar zum direkten Bearbeiten - Kontextmenü für leere Terminals (Input/Output Auswahl) - Block-Label mit Einheiten (40A 30mA statt 40A30mA) - Online-Status-Anzeige entfernt (funktionierte nicht zuverlässig) - Service Worker v5.2: Versionierte Assets nicht cachen Co-Authored-By: Claude Opus 4.5 --- admin/setup.php | 3 +- ajax/pwa_api.php | 81 ++- class/equipment.class.php | 12 +- class/equipmentconnection.class.php | 7 +- core/modules/modKundenKarte.class.php | 63 +- css/pwa.css | 308 +++++++- js/pwa.js | 963 +++++++++++++++++++++++--- pwa.php | 58 +- sw.js | 20 +- 9 files changed, 1389 insertions(+), 126 deletions(-) diff --git a/admin/setup.php b/admin/setup.php index 2708ce1..d357c15 100755 --- a/admin/setup.php +++ b/admin/setup.php @@ -403,7 +403,8 @@ print '

'; print '
'.$langs->trans("PWAMobileApp").'
'; print '

'; -$pwaUrl = dol_buildpath('/kundenkarte/pwa.php', 2); +// PWA URL mit vollständigem Pfad (ohne dol_buildpath wegen URL-Problemen) +$pwaUrl = DOL_URL_ROOT.'/custom/kundenkarte/pwa.php'; print ''; print ''; print ''; diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index 62ed4a1..bb5d072 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -71,6 +71,9 @@ if (!$user->hasRight('kundenkarte', 'read')) { exit; } +// Load language file for labels +$langs->loadLangs(array('kundenkarte@kundenkarte')); + // Load required classes require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/anlage.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php'; @@ -274,7 +277,8 @@ switch ($action) { 'width_te' => $eq->width_te, 'block_label' => $eq->getBlockLabel(), 'block_color' => $eq->getBlockColor(), - 'field_values' => $eq->getFieldValues() + 'field_values' => $eq->getFieldValues(), + 'fk_protection' => $eq->fk_protection > 0 ? (int) $eq->fk_protection : null ); } } @@ -287,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"; + $sql = "SELECT rowid, fk_source, output_label, 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"; @@ -346,6 +350,7 @@ switch ($action) { 'connection_type' => $obj->connection_type, 'color' => $obj->color, 'source_terminal_id' => $obj->source_terminal_id ?: '', + 'bundled_terminals' => $obj->bundled_terminals ?: '', 'is_top' => $isTop ); } @@ -374,6 +379,35 @@ switch ($action) { } } + // Verbindungen zwischen Equipment laden (mit path_data für 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 .= " 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 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 + ); + } + } + } + } + // Equipment-Typen für Response aufbereiten (bereits oben geladen) $typesData = array(); foreach ($types as $t) { @@ -384,7 +418,8 @@ switch ($action) { 'label_short' => $t->label_short, 'width_te' => $t->width_te, 'color' => $t->color, - 'category' => $t->category + 'category' => $t->category, + 'terminals_config' => $t->terminals_config ?: null ); } @@ -422,6 +457,7 @@ switch ($action) { $response['equipment'] = $equipmentData; $response['outputs'] = $outputsData; $response['inputs'] = $inputsData; + $response['connections'] = $connectionsData; $response['types'] = $typesData; $response['field_meta'] = $fieldMetaData; break; @@ -560,6 +596,32 @@ switch ($action) { } break; + // ============================================ + // GET PROTECTION DEVICES (FI/RCD für Anlage) + // ============================================ + case 'get_protection_devices': + $anlageId = GETPOSTINT('anlage_id'); + if ($anlageId <= 0) { + $response['error'] = 'Keine Anlage-ID'; + break; + } + + $equipment = new Equipment($db); + $devices = $equipment->fetchProtectionDevices($anlageId); + $result = array(); + foreach ($devices as $d) { + $result[] = array( + 'id' => $d->id, + 'label' => $d->label ?: $d->type_label, + 'type_label' => $d->type_label, + 'type_label_short' => $d->type_label_short, + 'display_label' => ($d->label ?: $d->type_label_short ?: $d->type_label).' (Pos. '.$d->position_te.')' + ); + } + $response['success'] = true; + $response['devices'] = $result; + break; + // ============================================ // CREATE EQUIPMENT // ============================================ @@ -574,6 +636,7 @@ switch ($action) { $label = GETPOST('label', 'alphanohtml'); $positionTe = GETPOSTINT('position_te') ?: 1; $fieldValues = GETPOST('field_values', 'nohtml'); + $fkProtection = GETPOSTINT('fk_protection'); if ($carrierId <= 0 || $typeId <= 0) { $response['error'] = 'Carrier-ID und Typ-ID erforderlich'; @@ -591,6 +654,7 @@ switch ($action) { $equipment->position_te = $positionTe; $equipment->width_te = $eqType->width_te ?: 1; $equipment->field_values = $fieldValues; + $equipment->fk_protection = $fkProtection > 0 ? $fkProtection : null; // Bezeichnung automatisch generieren wenn leer (wie Website) if (empty(trim($equipment->label ?? ''))) { @@ -678,6 +742,7 @@ switch ($action) { $equipmentId = GETPOSTINT('equipment_id'); $label = GETPOST('label', 'alphanohtml'); $fieldValues = GETPOST('field_values', 'nohtml'); + $fkProtection = GETPOSTINT('fk_protection'); if ($equipmentId <= 0) { $response['error'] = 'Keine Equipment-ID'; @@ -692,6 +757,7 @@ switch ($action) { $equipment->label = $label; $equipment->field_values = $fieldValues; + $equipment->fk_protection = $fkProtection > 0 ? $fkProtection : null; $result = $equipment->update($user); if ($result > 0) { @@ -752,6 +818,8 @@ switch ($action) { $mediumLength = GETPOST('medium_length', 'alphanohtml'); $sourceTerminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output'; $sourceTerminalId = GETPOST('source_terminal_id', 'alphanohtml'); + $targetTerminalId = GETPOST('target_terminal_id', 'alphanohtml'); + $bundledTerminals = GETPOST('bundled_terminals', 'alphanohtml'); // 'all' oder '0,1,2' if ($equipmentId <= 0) { $response['error'] = 'Keine Equipment-ID'; @@ -773,15 +841,19 @@ switch ($action) { if ($direction === 'input') { // Einspeisung: fk_source = NULL, fk_target = Equipment + // Terminal-Position: t1=oben, t2=unten (Sicherungsautomaten haben keine feste Richtung!) $conn->fk_target = $equipmentId; $conn->fk_source = null; $conn->target_terminal = 'input'; + $conn->target_terminal_id = $targetTerminalId ?: 't2'; // Default: unten } else { // Abgang: fk_source = Equipment, fk_target = NULL + // Terminal-Position: t1=oben, t2=unten (Sicherungsautomaten haben keine feste Richtung!) $conn->fk_source = $equipmentId; $conn->fk_target = null; $conn->source_terminal = $sourceTerminal; $conn->source_terminal_id = $sourceTerminalId ?: ($sourceTerminal === 'top' ? 't1' : 't2'); + $conn->bundled_terminals = $bundledTerminals ?: null; $conn->medium_type = $mediumType; $conn->medium_spec = $mediumSpec; $conn->medium_length = $mediumLength; @@ -829,6 +901,9 @@ switch ($action) { if (GETPOSTISSET('source_terminal_id')) { $conn->source_terminal_id = GETPOST('source_terminal_id', 'alphanohtml') ?: $conn->source_terminal_id; } + if (GETPOSTISSET('bundled_terminals')) { + $conn->bundled_terminals = GETPOST('bundled_terminals', 'alphanohtml') ?: null; + } $result = $conn->update($user); if ($result > 0) { diff --git a/class/equipment.class.php b/class/equipment.class.php index a761a3d..c74c5e9 100755 --- a/class/equipment.class.php +++ b/class/equipment.class.php @@ -508,7 +508,14 @@ class Equipment extends CommonObject foreach ($blockFields as $field) { if (isset($values[$field->field_code]) && $values[$field->field_code] !== '') { - $parts[] = $values[$field->field_code]; + $val = $values[$field->field_code]; + // Einheit hinzufügen für bekannte Felder (wie in Website JS kundenkarte.js:6613-6617) + if ($field->field_code === 'ampere') { + $val = $val . 'A'; + } elseif ($field->field_code === 'sensitivity') { + $val = $val . 'mA'; + } + $parts[] = $val; } } @@ -516,7 +523,8 @@ class Equipment extends CommonObject return $this->type_label_short ?: ''; } - return implode('', $parts); + // Mit Leerzeichen verbinden für bessere Lesbarkeit (z.B. "40A 30mA" statt "40A30mA") + return implode(' ', $parts); } /** diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php index b174cf3..b22924a 100755 --- a/class/equipmentconnection.class.php +++ b/class/equipmentconnection.class.php @@ -19,6 +19,7 @@ class EquipmentConnection extends CommonObject public $fk_source; public $source_terminal = 'output'; public $source_terminal_id; + public $bundled_terminals; // 'all' = alle Terminals belegt, '0,1,2' = spezifische Indizes, NULL = einzeln public $fk_target; public $target_terminal = 'input'; public $target_terminal_id; @@ -86,7 +87,7 @@ class EquipmentConnection extends CommonObject $this->db->begin(); $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; - $sql .= "entity, fk_source, source_terminal, source_terminal_id, fk_target, target_terminal, target_terminal_id,"; + $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 .= " medium_type, medium_spec, medium_length,"; $sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y, path_data,"; @@ -96,6 +97,7 @@ class EquipmentConnection extends CommonObject $sql .= ", ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL"); $sql .= ", '".$this->db->escape($this->source_terminal ?: 'output')."'"; $sql .= ", ".($this->source_terminal_id ? "'".$this->db->escape($this->source_terminal_id)."'" : "NULL"); + $sql .= ", ".($this->bundled_terminals ? "'".$this->db->escape($this->bundled_terminals)."'" : "NULL"); $sql .= ", ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL"); $sql .= ", '".$this->db->escape($this->target_terminal ?: 'input')."'"; $sql .= ", ".($this->target_terminal_id ? "'".$this->db->escape($this->target_terminal_id)."'" : "NULL"); @@ -164,6 +166,7 @@ class EquipmentConnection extends CommonObject $this->fk_source = $obj->fk_source; $this->source_terminal = $obj->source_terminal; $this->source_terminal_id = $obj->source_terminal_id; + $this->bundled_terminals = isset($obj->bundled_terminals) ? $obj->bundled_terminals : null; $this->fk_target = $obj->fk_target; $this->target_terminal = $obj->target_terminal; $this->target_terminal_id = $obj->target_terminal_id; @@ -219,6 +222,7 @@ class EquipmentConnection extends CommonObject $sql .= " fk_source = ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL"); $sql .= ", source_terminal = '".$this->db->escape($this->source_terminal ?: 'output')."'"; $sql .= ", source_terminal_id = ".($this->source_terminal_id ? "'".$this->db->escape($this->source_terminal_id)."'" : "NULL"); + $sql .= ", bundled_terminals = ".($this->bundled_terminals ? "'".$this->db->escape($this->bundled_terminals)."'" : "NULL"); $sql .= ", fk_target = ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL"); $sql .= ", target_terminal = '".$this->db->escape($this->target_terminal ?: 'input')."'"; $sql .= ", target_terminal_id = ".($this->target_terminal_id ? "'".$this->db->escape($this->target_terminal_id)."'" : "NULL"); @@ -315,6 +319,7 @@ class EquipmentConnection extends CommonObject $conn->fk_source = $obj->fk_source; $conn->source_terminal = $obj->source_terminal; $conn->source_terminal_id = $obj->source_terminal_id; + $conn->bundled_terminals = isset($obj->bundled_terminals) ? $obj->bundled_terminals : null; $conn->fk_target = $obj->fk_target; $conn->target_terminal = $obj->target_terminal; $conn->target_terminal_id = $obj->target_terminal_id; diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php index 12274cd..46c75e8 100755 --- a/core/modules/modKundenKarte.class.php +++ b/core/modules/modKundenKarte.class.php @@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '6.1'; + $this->version = '7.5'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -621,6 +621,12 @@ class modKundenKarte extends DolibarrModules // v5.2.0: Halbe TE-Breiten (4.5 TE für Neozed etc.) $this->migrate_v520_decimal_te(); + + // v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge + $this->migrate_v680_bundled_terminals(); + + // v6.8.0: Schutzgruppen-Zuordnung (fk_protection) + $this->migrate_v680_protection_groups(); } /** @@ -829,6 +835,61 @@ class modKundenKarte extends DolibarrModules } } + /** + * Migration v6.8.0: Gebündelte Terminals für Multi-Phasen-Abgänge + * Ermöglicht einen Abgang der alle Terminals eines breiten Equipment belegt + * z.B. 3-Phasen B16 Automat mit einem E-Herd-Abgang + */ + private function migrate_v680_bundled_terminals() + { + $table = MAIN_DB_PREFIX."kundenkarte_equipment_connection"; + + // Prüfen ob Tabelle existiert + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + return; + } + + // Prüfen ob Spalte bereits existiert + $resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'bundled_terminals'"); + if ($resql && $this->db->num_rows($resql) > 0) { + return; + } + + // Spalte hinzufügen: 'all' = alle Terminals, '0,1,2' = spezifische Indizes, NULL = einzeln + $this->db->query("ALTER TABLE ".$table." ADD COLUMN bundled_terminals varchar(50) DEFAULT NULL AFTER source_terminal_id"); + } + + /** + * Migration v6.8.0: Schutzgruppen-Zuordnung für Equipment + * Ermöglicht Zuordnung von Equipment zu einem Schutzgerät (FI/RCD) + * fk_protection = ID des schützenden Equipment + * protection_label = Optionales Label für die Gruppe + */ + private function migrate_v680_protection_groups() + { + $table = MAIN_DB_PREFIX."kundenkarte_equipment"; + + // Prüfen ob Tabelle existiert + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + return; + } + + // fk_protection Spalte + $resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_protection'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + $this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_protection integer DEFAULT NULL AFTER fk_product"); + $this->db->query("ALTER TABLE ".$table." ADD INDEX idx_equipment_protection (fk_protection)"); + } + + // protection_label Spalte + $resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'protection_label'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + $this->db->query("ALTER TABLE ".$table." ADD COLUMN protection_label varchar(64) DEFAULT NULL AFTER fk_protection"); + } + } + /** * Function called when module is disabled. * Remove from database constants, boxes and permissions from Dolibarr database. diff --git a/css/pwa.css b/css/pwa.css index ddb6e68..05d7669 100644 --- a/css/pwa.css +++ b/css/pwa.css @@ -360,6 +360,56 @@ body { opacity: 0.6; } +/* ============================================ + ZULETZT BEARBEITET + ============================================ */ + +.recent-section { + padding: 0 16px 16px; +} + +.recent-title { + font-size: 13px; + font-weight: 600; + color: var(--colortextmuted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 16px 0 10px; + padding-left: 4px; +} + +.recent-section .list { + padding: 0; + flex: none; + overflow: visible; +} + +.recent-section .list-item { + padding: 12px 14px; +} + +.recent-section .list-item-icon { + width: 38px; + height: 38px; +} + +.recent-section .list-item-icon svg { + width: 20px; + height: 20px; +} + +.recent-section .list-item-title { + font-size: 14px; +} + +.recent-section .list-item-subtitle { + font-size: 11px; +} + +#recent-customers.hidden { + display: none; +} + /* ============================================ LISTS ============================================ */ @@ -810,27 +860,29 @@ body { } .equipment-block-type { - font-size: 9px; + font-size: 7px; + font-weight: bold; + color: rgba(255,255,255,0.8); + line-height: 1; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.equipment-block-value { + font-size: 11px; font-weight: bold; color: #fff; line-height: 1.1; } -.equipment-block-value { - font-size: 13px; - font-weight: bold; - color: #fff; - line-height: 1.2; -} - .equipment-block-label { - font-size: 8px; - color: rgba(255,255,255,0.7); + font-size: 7px; + color: rgba(255,255,255,0.6); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - line-height: 1.1; + line-height: 1; } /* Equipment Block Text (einzelner Block-Label wie "B16") */ @@ -915,28 +967,169 @@ body { line-height: 1; } +/* Abgang-Label Zelle (Zeile 1 und 5) */ +.terminal-label-cell { + display: flex; + justify-content: center; + min-height: 20px; +} + +/* Obere Labels (Zeile 1): am unteren Rand ausrichten (zum Terminal hin) */ +.terminal-label-cell.label-row-top { + align-items: flex-end; +} + +/* Untere Labels (Zeile 5): am oberen Rand ausrichten (zum Terminal hin) */ +.terminal-label-cell.label-row-bottom { + align-items: flex-start; +} + +.terminal-label-cell.empty { + min-height: 4px; +} + /* Abgang-Label (vertikal) */ .terminal-label { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 9px; - font-weight: bold; + font-weight: 600; color: #fff; line-height: 1.1; - max-height: 90px; + max-height: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 2px 0; + padding: 2px 4px; + background: rgba(0,0,0,0.3); + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +/* Anklickbare Labels */ +.terminal-label-cell:not(.empty) { + cursor: pointer; + transition: transform 0.15s; +} + +.terminal-label-cell:not(.empty):active { + transform: scale(0.95); +} + +.terminal-label-cell:not(.empty):active .terminal-label { + background: rgba(173, 140, 79, 0.4); } .terminal-label .cable-info { font-weight: normal; - font-size: 8px; - color: #888; + font-size: 7px; + color: rgba(255,255,255,0.6); + display: block; } -/* Output-Zeile braucht mehr Platz wenn Labels vorhanden */ +/* ============================================ + GEBÜNDELTE TERMINALS (Multi-Phasen-Abgänge) + ============================================ */ + +/* Gebündeltes Label: Zentriert über alle Spalten */ +.terminal-label-cell.bundled-label { + display: flex; + justify-content: center; + align-items: center; +} + +/* Gebündeltes Label mit Pfeil */ +.terminal-label-cell.bundled-with-arrow { + cursor: pointer; +} + +.bundled-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +/* Oben: Label zuerst, Pfeil unten (zeigt zum Automaten) */ +.label-row-top .bundled-content { + flex-direction: column; +} + +/* Unten: Pfeil oben (zeigt zum Automaten), Label unten */ +.bundled-content-bottom { + flex-direction: column; +} + +.bundled-arrow { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; +} + +.bundled-arrow .terminal-phase { + font-size: 8px; + font-weight: bold; + color: rgba(255,255,255,0.7); +} + +/* Platzhalter für gebündelte Terminals (keine Pfeile mehr in Zeile 2/4) */ +.terminal-point.bundled-placeholder { + min-height: 8px; +} + +/* Hauptterminal bei Bündelung */ +.terminal-point.bundled-main { + position: relative; + z-index: 2; +} + +/* Rand-Terminals bei Bündelung: Gedimmt */ +.terminal-point.bundled-edge { + opacity: 0.6; + pointer-events: none; +} + +/* Terminal-Connector wird nicht mehr angezeigt */ +.terminal-connector { + display: none; +} + +/* ============================================ + VERBINDUNGSLINIEN (SVG Overlay) + ============================================ */ + +.connection-lines-svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; + overflow: visible; +} + +.connection-shadow { + fill: none; + stroke: rgba(0, 0, 0, 0.3); + stroke-width: 5; + stroke-linecap: round; + stroke-linejoin: round; +} + +.connection-line { + fill: none; + stroke: var(--colortextmuted); + stroke-width: 2.5; + stroke-linecap: round; + stroke-linejoin: round; +} + +/* Grid-Rows: 1=Labels oben, 2=Terminals oben, 3=Equipment, 4=Terminals unten, 5=Labels unten */ /* Add Button in Carrier (letzte Spalte, Zeile 2) */ .btn-add-equipment { display: flex; @@ -1179,6 +1372,25 @@ body { margin-bottom: 14px; } +/* Protection Section (FI/RCD-Zuordnung) */ +.protection-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--colorborder); +} + +.protection-section label { + display: flex; + align-items: center; + gap: 8px; +} + +.protection-section .icon-small { + width: 16px; + height: 16px; + fill: var(--butactionbg); +} + #eq-dynamic-fields .form-select { width: 100%; padding: 12px; @@ -1228,6 +1440,12 @@ body { height: 20px; } +.hint-text { + font-size: 12px; + color: var(--colortextmuted); + margin-top: 4px; +} + .step-label { font-size: 14px; color: var(--colortextmuted); @@ -1349,11 +1567,9 @@ body { .phase-btn[data-type="L3"].selected { border-color: #888; background: rgba(100,100,100,0.2); } .phase-btn[data-type="N"].selected { border-color: #0066cc; background: rgba(0,102,204,0.2); } .phase-btn[data-type="PE"].selected { border-color: #27ae60; background: rgba(39,174,96,0.2); } -.phase-btn[data-type="L1N"].selected { border-color: #8B4513; background: rgba(139,69,19,0.2); } +.phase-btn[data-type="LN"].selected { border-color: #8B4513; background: rgba(139,69,19,0.2); } .phase-btn[data-type="3P"].selected { border-color: #e74c3c; background: rgba(231,76,60,0.2); } .phase-btn[data-type="3P+N"].selected { border-color: #e74c3c; background: rgba(231,76,60,0.2); } -.phase-btn[data-type="L2N"].selected { border-color: #555; background: rgba(50,50,50,0.3); } -.phase-btn[data-type="L3N"].selected { border-color: #888; background: rgba(100,100,100,0.2); } .phase-btn[data-type="DATA"].selected { border-color: #9b59b6; background: rgba(155,89,182,0.2); } /* Abgangsseite-Buttons */ @@ -1784,3 +2000,55 @@ body { min-height: 44px; } } + +/* ============================================ + TERMINAL KONTEXTMENÜ + ============================================ */ + +.terminal-context-menu { + background: var(--colorbacktitle); + border: 1px solid var(--colorborder); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + overflow: hidden; + min-width: 200px; +} + +.tcm-item { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + cursor: pointer; + color: var(--colortext); + transition: background 0.15s; +} + +.tcm-item:not(:last-child) { + border-bottom: 1px solid var(--colorborder); +} + +.tcm-item:hover, +.tcm-item:active { + background: rgba(255, 255, 255, 0.1); +} + +.tcm-icon { + font-size: 16px; + width: 20px; + text-align: center; +} + +/* Leere Terminals - neutral */ +.terminal-empty { + cursor: pointer; +} + +.terminal-dot-empty { + width: 10px; + height: 10px; + border-radius: 50%; + background: transparent; + border: 2px dashed rgba(255, 255, 255, 0.25); +} + diff --git a/js/pwa.js b/js/pwa.js index 141acc5..8ed3682 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -29,6 +29,7 @@ equipmentTypes: [], outputs: [], inputs: [], + connections: [], fieldMeta: {}, // Offline queue @@ -46,6 +47,7 @@ connectionDirection: 'output', // 'output' oder 'input' mediumTypes: null, // Kabeltypen aus DB (gecacht) cachedTypeFields: null, // Equipment-Felder Cache + protectionDevices: [], // FI/RCD-Geräte für aktuelle Anlage }; @@ -73,6 +75,11 @@ showOfflineBar(); }); + // Offline-Bar anzeigen falls nicht online + if (!navigator.onLine) { + showOfflineBar(); + } + // Check stored auth const storedToken = localStorage.getItem('kundenkarte_pwa_token'); const storedUser = localStorage.getItem('kundenkarte_pwa_user'); @@ -200,8 +207,10 @@ $('#btn-detail-close').on('click', () => $('#sheet-equipment-detail').removeClass('active')); $('#sheet-equipment-detail .sheet-overlay').on('click', () => $('#sheet-equipment-detail').removeClass('active')); - // Terminal/Connection - Klick auf einzelne Klemme - $('#editor-content').on('click', '.terminal-point', handleTerminalClick); + // Terminal/Connection - Klick auf einzelne Klemme (inkl. leere Terminals) + $('#editor-content').on('click', '.terminal-point, .terminal-empty', handleTerminalClick); + // Terminal-Labels anklickbar zum Bearbeiten + $('#editor-content').on('click', '.terminal-label-cell:not(.empty)', handleTerminalLabelClick); $('#btn-save-connection').on('click', handleSaveConnection); $('#btn-delete-connection').on('click', handleDeleteConnectionConfirm); // Abgangsseite-Buttons @@ -210,6 +219,9 @@ $(this).addClass('selected'); }); + // Medium-Type Change -> Spezifikationen laden + $('#conn-medium-type').on('change', handleMediumTypeChange); + // Bestätigungsdialog $('#btn-confirm-ok').on('click', function() { closeModal('confirm'); @@ -300,6 +312,11 @@ // State speichern für Refresh-Wiederherstellung saveState(name); + + // Zuletzt bearbeitete Kunden laden wenn Search-Screen + if (name === 'search') { + loadRecentCustomers(); + } } // Zustand in sessionStorage speichern @@ -315,6 +332,65 @@ sessionStorage.setItem('kundenkarte_pwa_state', JSON.stringify(state)); } + // ============================================ + // ZULETZT BEARBEITETE KUNDEN + // ============================================ + + const MAX_RECENT_CUSTOMERS = 5; + + function addToRecentCustomers(id, name, address) { + let recent = JSON.parse(localStorage.getItem('kundenkarte_recent_customers') || '[]'); + + // Entferne den Kunden falls schon vorhanden (wird neu an den Anfang gesetzt) + recent = recent.filter(c => c.id !== id); + + // Füge an den Anfang hinzu + recent.unshift({ + id: id, + name: name, + address: address, + timestamp: Date.now() + }); + + // Begrenze auf MAX_RECENT_CUSTOMERS + recent = recent.slice(0, MAX_RECENT_CUSTOMERS); + + localStorage.setItem('kundenkarte_recent_customers', JSON.stringify(recent)); + } + + function loadRecentCustomers() { + const recent = JSON.parse(localStorage.getItem('kundenkarte_recent_customers') || '[]'); + + if (recent.length === 0) { + $('#recent-customers').addClass('hidden'); + return; + } + + $('#recent-customers').removeClass('hidden'); + + let html = ''; + recent.forEach(c => { + html += ` +
+
+ + + +
+
+
${escapeHtml(c.name)}
+
${escapeHtml(c.address || '')}
+
+
+ `; + }); + + $('#recent-list').html(html); + + // Click-Handler für die Kunden + $('#recent-list .list-item').on('click', handleCustomerSelect); + } + // Anlagen-Liste für aktuellen Kunden neu laden async function reloadAnlagen() { if (!App.customerId) return; @@ -417,6 +493,9 @@ App.customerAddress = address; $('#customer-name').text(name); + // Zu "Zuletzt bearbeitet" hinzufügen + addToRecentCustomers(id, name, address); + showScreen('anlagen'); $('#anlagen-list').html('
'); @@ -654,6 +733,7 @@ App.equipmentTypes = response.types || []; App.outputs = response.outputs || []; App.inputs = response.inputs || []; + App.connections = response.connections || []; App.fieldMeta = response.field_meta || {}; // Cache for offline @@ -664,9 +744,13 @@ types: App.equipmentTypes, outputs: App.outputs, inputs: App.inputs, + connections: App.connections, fieldMeta: App.fieldMeta })); + // Protection devices laden (FI/RCD) + await loadProtectionDevices(); + renderEditor(); } } catch (err) { @@ -680,6 +764,7 @@ App.equipmentTypes = data.types || []; App.outputs = data.outputs || []; App.inputs = data.inputs || []; + App.connections = data.connections || []; App.fieldMeta = data.fieldMeta || {}; renderEditor(); showToast('Offline - Zeige gecachte Daten', 'warning'); @@ -689,6 +774,38 @@ } } + /** + * Lädt FI/RCD-Schutzgeräte für die aktuelle Anlage + */ + async function loadProtectionDevices() { + if (!App.anlageId || !App.isOnline) return; + + try { + const response = await apiCall('ajax/pwa_api.php', { + action: 'get_protection_devices', + anlage_id: App.anlageId + }); + if (response.success) { + App.protectionDevices = response.devices || []; + } + } catch (err) { + // Kein Fehler anzeigen - Dropdown bleibt leer + } + } + + /** + * Befüllt das Protection-Dropdown im Equipment-Dialog + */ + function populateProtectionDropdown(selectedId) { + const $select = $('#equipment-protection'); + $select.find('option:not(:first)').remove(); + + App.protectionDevices.forEach(device => { + const selected = device.id == selectedId ? ' selected' : ''; + $select.append(``); + }); + } + function renderEditor() { if (!App.panels.length) { $('#editor-content').html('
Noch keine Felder angelegt.
Tippe auf "+ Feld" um zu beginnen.
'); @@ -725,40 +842,126 @@
`; - // === Zeile 1: Terminals oben (Inputs + Top-Outputs) === + // === Zeile 1: Abgang-Labels OBEN (nur wenn Abgang oben ist) === 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) : []; const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : []; - for (let t = 0; t < widthTe; t++) { - const colPos = posTe > 0 ? posTe + t : 0; - const style = `grid-row:1;${colPos > 0 ? ' grid-column:' + colPos : ''}`; - const inp = eqInputs[t] || null; - const topOut = eqTopOutputs[t] || null; + // Terminal-Anzahl aus terminals_config ermitteln (Fallback auf widthTe) + const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); + const topTerminalCount = getTerminalCount(type, 'top', widthTe); - if (topOut) { - // Top-Output: Pfeil nach oben ▲ - const phaseColor = topOut.color || getPhaseColor(topOut.connection_type); - html += ``; - html += renderOutputLabel(topOut, phaseColor, 'up'); - html += ``; - } else if (inp) { - const phaseColor = inp.color || getPhaseColor(inp.connection_type); - html += ``; - html += ``; - html += `${escapeHtml(inp.connection_type || '')}`; - html += ``; - } else { - html += ``; - html += ``; + // Gebündelter Abgang? (alle Terminals eines breiten Equipment belegt) + const bundledTop = eqTopOutputs.find(o => o.bundled_terminals === 'all'); + + if (bundledTop && widthTe > 1) { + // Gebündeltes Label mit Pfeil: Über alle Spalten des Equipment spannen + const gridColStyle = posTe > 0 + ? `grid-row:1; grid-column: ${posTe} / span ${widthTe}` + : `grid-row:1; grid-column: span ${widthTe}`; + const cableInfo = buildCableInfo(bundledTop); + const phaseColor = bundledTop.color || getPhaseColor(bundledTop.connection_type); + html += ``; + // Container für vertikale Anordnung: Label oben, Pfeil unten + html += ``; + if (bundledTop.output_label) { + html += `${escapeHtml(bundledTop.output_label)}`; + if (cableInfo) html += `${escapeHtml(cableInfo)}`; html += ``; } + html += ``; + html += ``; + if (bundledTop.connection_type) html += `${escapeHtml(bundledTop.connection_type)}`; + html += ``; + html += ``; + html += ``; + } else { + // Normale einzelne Labels pro Terminal - nur für tatsächliche Terminals + 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; + + if (topOut && topOut.output_label && (!topOut.bundled_terminals || widthTe <= 1)) { + const cableInfo = buildCableInfo(topOut); + html += ``; + html += `${escapeHtml(topOut.output_label)}`; + if (cableInfo) html += `${escapeHtml(cableInfo)}`; + html += ``; + html += ``; + } else { + html += ``; + } + } + // 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:1;${colPos > 0 ? ' grid-column:' + colPos : ''}`; + html += ``; + } } }); - // === Zeile 2: Equipment-Blöcke === + // === Zeile 2: Terminal-Punkte OBEN (direkt am Equipment) === + 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); + + // 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; + + if (bundledTop && widthTe > 1) { + // Gebündelter Abgang: Pfeil ist bereits im Label (Zeile 1) - hier nur leere Zelle + html += ``; + } else if (topOut && (!topOut.bundled_terminals || widthTe <= 1)) { + // Normaler Top-Output ODER bundled bei 1 TE (Bundle macht bei 1 TE keinen Unterschied) + const phaseColor = topOut.color || getPhaseColor(topOut.connection_type); + html += ``; + html += ``; + html += `${escapeHtml(topOut.connection_type || '')}`; + html += ``; + } else if (inp) { + const phaseColor = inp.color || getPhaseColor(inp.connection_type); + html += ``; + html += ``; + html += `${escapeHtml(inp.connection_type || '')}`; + html += ``; + } else { + // Leerer Terminal - neutral, Position "top" + html += ``; + html += ``; + html += ``; + } + } + // Leere Zellen für restliche TE-Breite (ohne Terminal-Punkte) + for (let t = topTerminalCount; t < widthTe; t++) { + const colPos = posTe > 0 ? posTe + t : 0; + const style = `grid-row:2;${colPos > 0 ? ' grid-column:' + colPos : ''}`; + html += ``; + } + }); + + // === Zeile 3: Equipment-Blöcke === + // Ermittle welche Equipment als Schutzgeräte dienen (werden von anderen referenziert) + const protectionDeviceIds = new Set(); + carrierEquipment.forEach(eq => { + if (eq.fk_protection) protectionDeviceIds.add(eq.fk_protection); + }); + carrierEquipment.forEach(eq => { const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); const widthTe = parseFloat(eq.width_te) || 1; @@ -771,11 +974,30 @@ const showBlockFields = blockFields && blockFields !== typeLabel && blockFields !== (type?.ref || ''); const gridCol = posTe > 0 - ? `grid-row:2; grid-column: ${posTe} / span ${widthTe}` - : `grid-row:2; grid-column: span ${widthTe}`; + ? `grid-row:3; grid-column: ${posTe} / span ${widthTe}` + : `grid-row:3; grid-column: span ${widthTe}`; + + // Schutzgruppen-Darstellung + let protectionStyle = ''; + let protectionClass = ''; + + // 1. Ist dieses Equipment ein Schutzgerät? (wird von anderen referenziert) + const isProtectionDevice = protectionDeviceIds.has(eq.id); + if (isProtectionDevice) { + const deviceColor = getProtectionColor(eq.id); + protectionStyle = `border-left: 4px solid ${deviceColor};`; + protectionClass = ' is-protection-device'; + } + + // 2. Ist dieses Equipment einem Schutzgerät zugeordnet? + if (eq.fk_protection) { + const protectionColor = getProtectionColor(eq.fk_protection); + protectionStyle += `border-bottom: 3px solid ${protectionColor};`; + protectionClass += ' has-protection'; + } html += ` -
+
${escapeHtml(typeLabel)} ${showBlockFields ? `${escapeHtml(blockFields)}` : ''} ${escapeHtml(eqLabel)} @@ -783,34 +1005,123 @@ `; }); - // +-Button in letzter Spalte (auto), Zeile 2 + // +-Button in letzter Spalte (auto), Zeile 3 html += ` - `; - // === Zeile 3: Output-Terminals unten (Standard-Abgänge) === + // === Zeile 4: Terminal-Punkte UNTEN (direkt am Equipment) === + 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); + + // 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; + + if (bundledBottom && widthTe > 1) { + // Gebündelter Abgang: Pfeil ist im Label (Zeile 5) - hier nur leere Zelle + html += ``; + } else if (out && (!out.bundled_terminals || widthTe <= 1)) { + // Normaler Abgang ODER bundled bei 1 TE (Bundle macht bei 1 TE keinen Unterschied) + const phaseColor = out.color || getPhaseColor(out.connection_type); + html += ``; + html += ``; + html += `${escapeHtml(out.connection_type || '')}`; + html += ``; + } else if (inp) { + const phaseColor = inp.color || getPhaseColor(inp.connection_type); + html += ``; + html += ``; + html += `${escapeHtml(inp.connection_type || '')}`; + html += ``; + } else { + // Leerer Terminal - neutral, Position "bottom" + html += ``; + html += ``; + html += ``; + } + } + // Leere Zellen für restliche TE-Breite (ohne Terminal-Punkte) + for (let t = bottomTerminalCount; t < widthTe; t++) { + const colPos = posTe > 0 ? posTe + t : 0; + const style = `grid-row:4;${colPos > 0 ? ' grid-column:' + colPos : ''}`; + html += ``; + } + }); + + // === Zeile 5: Abgang-Labels UNTEN (nur wenn Abgang unten ist) === 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) : []; - for (let t = 0; t < widthTe; t++) { - const colPos = posTe > 0 ? posTe + t : 0; - const style = `grid-row:3;${colPos > 0 ? ' grid-column:' + colPos : ''}`; - const out = eqBottomOutputs[t] || null; + // Terminal-Anzahl aus terminals_config ermitteln + const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); + const bottomTerminalCount = getTerminalCount(type, 'bottom', widthTe); - if (out) { - const phaseColor = out.color || getPhaseColor(out.connection_type); - html += ``; - html += renderOutputLabel(out, phaseColor, 'down'); - html += ``; - } else { - html += ``; - html += ``; + // Gebündelter Abgang? + const bundledBottom = eqBottomOutputs.find(o => o.bundled_terminals === 'all'); + + if (bundledBottom && widthTe > 1) { + // Gebündeltes Label mit Pfeil: Über alle Spalten des Equipment spannen + const gridColStyle = posTe > 0 + ? `grid-row:5; grid-column: ${posTe} / span ${widthTe}` + : `grid-row:5; grid-column: span ${widthTe}`; + const cableInfo = buildCableInfo(bundledBottom); + const phaseColor = bundledBottom.color || getPhaseColor(bundledBottom.connection_type); + html += ``; + // Container für vertikale Anordnung: Pfeil oben, Label unten + html += ``; + html += ``; + html += ``; + if (bundledBottom.connection_type) html += `${escapeHtml(bundledBottom.connection_type)}`; + html += ``; + if (bundledBottom.output_label) { + html += `${escapeHtml(bundledBottom.output_label)}`; + if (cableInfo) html += `${escapeHtml(cableInfo)}`; html += ``; } + html += ``; + html += ``; + } else { + // Normale einzelne Labels pro Terminal - nur für tatsächliche Terminals + 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; + + if (out && out.output_label && (!out.bundled_terminals || widthTe <= 1)) { + const cableInfo = buildCableInfo(out); + html += ``; + html += `${escapeHtml(out.output_label)}`; + if (cableInfo) html += `${escapeHtml(cableInfo)}`; + html += ``; + html += ``; + } else { + html += ``; + } + } + // 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:5;${colPos > 0 ? ' grid-column:' + colPos : ''}`; + html += ``; + } } }); @@ -829,10 +1140,67 @@ $('#editor-content').html(html); + // Render connection lines (SVG overlay) + renderConnectionLines(); + // Load type grid renderTypeGrid(); } + /** + * Render SVG connection lines from path_data + * Only shows connections that were manually drawn on the website + */ + function renderConnectionLines() { + if (!App.connections || App.connections.length === 0) { + return; + } + + // Für jede Hutschiene ein SVG-Overlay erstellen + $('.carrier-card').each(function() { + const $carrier = $(this); + const carrierId = $carrier.find('.carrier-header').data('carrier-id'); + const $content = $carrier.find('.carrier-content'); + + if (!$content.length) return; + + // Equipment dieser Hutschiene finden + const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId); + const equipmentIds = carrierEquipment.map(e => e.id); + + // Verbindungen filtern die zu dieser Hutschiene gehören + const carrierConnections = App.connections.filter(c => + equipmentIds.includes(parseInt(c.fk_source)) || + equipmentIds.includes(parseInt(c.fk_target)) + ); + + if (carrierConnections.length === 0) return; + + // SVG-Container erstellen falls nicht vorhanden + let $svg = $carrier.find('.connection-lines-svg'); + if (!$svg.length) { + $svg = $(''); + $carrier.css('position', 'relative'); + $carrier.append($svg); + } + + // SVG-Inhalt generieren + let svgContent = ''; + carrierConnections.forEach(conn => { + if (!conn.path_data) return; + + const color = conn.color || getPhaseColor(conn.connection_type); + + // Schatten-Pfad für bessere Sichtbarkeit + svgContent += ``; + // Hauptpfad + svgContent += ``; + }); + + $svg.html(svgContent); + }); + } + function renderTypeGrid() { const categoryLabels = { 'automat': 'Leitungsschutz', @@ -1160,6 +1528,11 @@ // Felder vom Server laden await loadTypeFields(App.selectedTypeId, App.editEquipmentId); + // Protection-Dropdown befüllen (leer für neues Equipment) + if (!App.editEquipmentId) { + populateProtectionDropdown(null); + } + // Zu Schritt 2 wechseln showEquipmentStep('fields'); } @@ -1354,13 +1727,16 @@ return; } + const fkProtection = parseInt($('#equipment-protection').val()) || 0; + const data = { action: 'create_equipment', carrier_id: App.currentCarrierId, type_id: App.selectedTypeId, label: label, position_te: nextPos, - field_values: JSON.stringify(fieldValues) + field_values: JSON.stringify(fieldValues), + fk_protection: fkProtection }; closeModal('add-equipment'); @@ -1378,7 +1754,8 @@ width_te: type?.width_te || 1, field_values: fieldValues, block_label: response.block_label || '', - block_color: response.block_color || type?.color || '' + block_color: response.block_color || type?.color || '', + fk_protection: fkProtection || null }); renderEditor(); showToast('Automat angelegt', 'success'); @@ -1409,11 +1786,14 @@ * Bestehenden Automaten aktualisieren */ async function saveEquipmentUpdate(label, fieldValues) { + const fkProtection = parseInt($('#equipment-protection').val()) || 0; + const data = { action: 'update_equipment', equipment_id: App.editEquipmentId, label: label, - field_values: JSON.stringify(fieldValues) + field_values: JSON.stringify(fieldValues), + fk_protection: fkProtection }; closeModal('add-equipment'); @@ -1429,6 +1809,7 @@ eq.field_values = fieldValues; eq.block_label = response.block_label || ''; eq.block_color = response.block_color || eq.block_color; + eq.fk_protection = fkProtection || null; } renderEditor(); showToast('Automat aktualisiert', 'success'); @@ -1443,6 +1824,7 @@ if (eq) { eq.label = label; eq.field_values = fieldValues; + eq.fk_protection = fkProtection || null; } renderEditor(); } @@ -1452,6 +1834,7 @@ if (eq) { eq.label = label; eq.field_values = fieldValues; + eq.fk_protection = fkProtection || null; } renderEditor(); showToast('Wird synchronisiert...', 'warning'); @@ -1558,6 +1941,87 @@ html += '
'; } + // Verbindungen zu anderen Equipment (connections mit path_data von Website) + const connectionsFrom = App.connections ? App.connections.filter(c => c.fk_source == eq.id) : []; + const connectionsTo = App.connections ? App.connections.filter(c => c.fk_target == eq.id) : []; + + if (connectionsFrom.length || connectionsTo.length) { + html += '
'; + html += '
Verbindungen
'; + html += '
'; + + // Verbindungen VON diesem Equipment + connectionsFrom.forEach(c => { + const targetEq = App.equipment.find(e => e.id == c.fk_target); + const targetLabel = targetEq?.label || targetEq?.block_label || 'Equipment ' + c.fk_target; + const color = c.color || getPhaseColor(c.connection_type); + html += `
+ +
+
→ ${escapeHtml(targetLabel)}
+
${escapeHtml(c.connection_type || '')}
+
+
`; + }); + + // Verbindungen ZU diesem Equipment + connectionsTo.forEach(c => { + const sourceEq = App.equipment.find(e => e.id == c.fk_source); + const sourceLabel = sourceEq?.label || sourceEq?.block_label || 'Equipment ' + c.fk_source; + const color = c.color || getPhaseColor(c.connection_type); + html += `
+ +
+
← ${escapeHtml(sourceLabel)}
+
${escapeHtml(c.connection_type || '')}
+
+
`; + }); + + html += '
'; + } + + // Schutzgerät-Zuordnung (fk_protection) + if (eq.fk_protection) { + const protectionEq = App.equipment.find(e => e.id == eq.fk_protection); + const protectionColor = getProtectionColor(eq.fk_protection); + if (protectionEq) { + const protLabel = protectionEq.label || protectionEq.block_label || 'Schutzgerät'; + const protType = App.equipmentTypes.find(t => t.id == protectionEq.fk_equipment_type); + const protTypeLabel = protType?.label_short || protType?.label || ''; + html += '
'; + html += '
Schutzeinrichtung
'; + html += '
'; + html += `
+ +
+
${escapeHtml(protLabel)}
+
${escapeHtml(protTypeLabel)} ${escapeHtml(protectionEq.block_label || '')}
+
+
`; + html += '
'; + } + } + + // Geschützte Geräte (wenn dieses Equipment ein Schutzgerät ist) + const protectedEquipment = App.equipment.filter(e => e.fk_protection == eq.id); + if (protectedEquipment.length) { + const protectionColor = getProtectionColor(eq.id); + html += '
'; + html += '
Schützt
'; + html += '
'; + protectedEquipment.forEach(pe => { + const peLabel = pe.label || pe.block_label || 'Equipment'; + html += `
+ +
+
${escapeHtml(peLabel)}
+
+
`; + }); + html += '
'; + } + // Position-Info const carrier = App.carriers.find(c => c.id == eq.fk_carrier); html += '
'; @@ -1610,6 +2074,9 @@ await loadTypeFields(eq.fk_equipment_type, eqId); + // Protection-Dropdown befüllen mit aktuellem Wert + populateProtectionDropdown(eq.fk_protection); + showEquipmentStep('fields'); openModal('add-equipment'); } @@ -1711,9 +2178,122 @@ // CONNECTION (TERMINAL) ACTIONS // ============================================ + /** + * Kabel-Info aus Connection zusammenbauen + */ + function buildCableInfo(conn) { + const parts = []; + if (conn.medium_type) parts.push(conn.medium_type); + if (conn.medium_spec) parts.push(conn.medium_spec); + if (conn.medium_length) parts.push('(' + conn.medium_length + ')'); + return parts.join(' '); + } + + /** + * Click-Handler für Terminal-Labels (zum Bearbeiten) + */ + function handleTerminalLabelClick(e) { + e.stopPropagation(); + const $cell = $(this); + const connId = $cell.data('connection-id'); + const eqId = $cell.data('equipment-id'); + const direction = $cell.data('direction') || 'output'; + + if (!connId) return; + + // Connection aus App-State finden + const conn = direction === 'input' + ? App.inputs.find(i => i.id == connId) + : App.outputs.find(o => o.id == connId); + + if (!conn) return; + + // Terminal-Position ermitteln + const terminalPosition = conn.is_top ? 'top' : 'bottom'; + + // Connection-Bearbeitungsmodus mit vorhandenen Daten + openEditConnectionDialog(eqId, direction, terminalPosition, conn); + } + + /** + * Connection-Dialog im Bearbeitungsmodus öffnen + */ + async function openEditConnectionDialog(eqId, direction, terminalPosition, conn) { + App.connectionEquipmentId = eqId; + App.connectionDirection = direction; + App.connectionTerminalPosition = terminalPosition; + App.editConnectionId = conn.id; + + renderTypeSelect(direction, conn.connection_type || ''); + $('#connection-modal-title').text(direction === 'input' ? 'Anschlusspunkt bearbeiten' : 'Abgang bearbeiten'); + $('#btn-delete-connection').removeClass('hidden'); + $('#conn-color').val(conn.color || '#3498db'); + $('#conn-label').val(conn.output_label || ''); + $('#conn-medium-length').val(conn.medium_length || ''); + + // Medium-Typen laden und Select befüllen + await loadMediumTypes(); + renderMediumTypeSelect(conn.medium_type || ''); + + // Medium-Spec laden falls Typ gewählt + if (conn.medium_type) { + // Trigger change um Specs zu laden, dann Wert setzen + handleMediumTypeChange(); + if (conn.medium_spec) { + $('#conn-medium-spec').val(conn.medium_spec); + } + } else { + $('#conn-medium-spec').html(''); + } + + // Side-Button auf aktuelle Terminal-Position setzen + setSideButton(terminalPosition); + + // Side-Buttons immer zeigen + $('#conn-side-fields').show(); + // Medium-Felder nur bei Abgang zeigen + $('#conn-output-fields').toggle(direction === 'output'); + + // Bundle-Option: Nur bei Abgang + Equipment mit mehr als 1 Terminal + const eq = App.equipment ? App.equipment.find(e => e.id == eqId) : null; + const type = eq ? App.equipmentTypes.find(t => t.id == eq.fk_equipment_type) : null; + // terminalPosition kommt bereits als Parameter + const terminalCount = getTerminalCount(type, terminalPosition, parseFloat(eq?.width_te) || 1); + if (direction === 'output' && terminalCount > 1) { + $('#conn-bundle-fields').removeClass('hidden'); + $('#conn-bundle-all').prop('checked', conn.bundled_terminals === 'all'); + } else { + $('#conn-bundle-fields').addClass('hidden'); + } + + openModal('connection'); + } + // Phasen-Optionen wie auf der Website const INPUT_PHASES = ['L1', 'L2', 'L3', '3P', '3P+N', 'PE']; - const OUTPUT_PHASES = ['L1N', 'L2N', 'L3N', 'N', '3P+N', 'PE', 'DATA']; + const OUTPUT_PHASES = ['LN', 'N', '3P+N', 'PE', 'DATA']; + + /** + * Terminal-Anzahl aus Equipment-Typ ermitteln + * @param {object} type - Equipment-Typ mit terminals_config + * @param {string} position - 'top' oder 'bottom' + * @param {number} fallback - Fallback-Wert (normalerweise width_te) + * @returns {number} Anzahl der Terminals + */ + function getTerminalCount(type, position, fallback) { + if (!type || !type.terminals_config) return fallback; + try { + const config = typeof type.terminals_config === 'string' + ? JSON.parse(type.terminals_config) + : type.terminals_config; + if (config.terminals && Array.isArray(config.terminals)) { + return config.terminals.filter(t => t.pos === position).length; + } + } catch (e) { + // JSON-Parse-Fehler ignorieren + } + return fallback; + } /** * Phasenfarbe ermitteln (DIN VDE Farben) @@ -1722,12 +2302,37 @@ const colors = { 'L1': '#8B4513', 'L2': '#1a1a1a', 'L3': '#666', 'N': '#0066cc', 'PE': '#27ae60', - 'L1N': '#8B4513', 'L2N': '#1a1a1a', 'L3N': '#666', + 'LN': '#8B4513', // Phase+Neutral - braun wie L1 + 'L1N': '#8B4513', 'L2N': '#1a1a1a', 'L3N': '#666', // Legacy '3P': '#e74c3c', '3P+N': '#e74c3c', 'DATA': '#9b59b6' }; return colors[type] || '#888'; } + /** + * Schutzgruppen-Farbe ermitteln (eindeutig pro protection_id) + */ + const protectionColorCache = {}; + function getProtectionColor(protectionId) { + if (!protectionId) return null; + if (protectionColorCache[protectionId]) return protectionColorCache[protectionId]; + + // Helle, gut sichtbare Farben für Schutzgruppen + const colors = [ + '#e74c3c', // Rot + '#3498db', // Blau + '#f39c12', // Orange + '#9b59b6', // Lila + '#1abc9c', // Türkis + '#e91e63', // Pink + '#00bcd4', // Cyan + '#ff5722', // Deep Orange + ]; + const idx = Object.keys(protectionColorCache).length % colors.length; + protectionColorCache[protectionId] = colors[idx]; + return colors[idx]; + } + /** * Abgangsseite-Button setzen */ @@ -1757,24 +2362,25 @@ } /** - * Klick auf Terminal-Zelle (Input oder Output) + * Klick auf Terminal-Zelle + * Bei vorhandener Verbindung: direkt bearbeiten + * Bei leerem Terminal: Kontextmenü mit Wahl Input/Output */ function handleTerminalClick(e) { e.stopPropagation(); const $point = $(this); const eqId = $point.data('equipment-id'); - const direction = $point.data('direction'); + const terminalPosition = $point.data('terminal-position'); // 'top' oder 'bottom' const connId = $point.data('connection-id'); - App.connectionEquipmentId = eqId; - App.connectionDirection = direction; - - // Typ-Select befüllen - renderTypeSelect(direction, ''); - + // Bestehende Verbindung? -> Direkt bearbeiten if (connId) { - // Bearbeiten + const direction = $point.data('direction'); + App.connectionEquipmentId = eqId; + App.connectionDirection = direction; + App.connectionTerminalPosition = terminalPosition; App.editConnectionId = connId; + $('#connection-modal-title').text('Verbindung bearbeiten'); $('#btn-delete-connection').removeClass('hidden'); @@ -1786,27 +2392,212 @@ renderTypeSelect(direction, conn.connection_type); $('#conn-color').val(conn.color || '#3498db'); $('#conn-label').val(conn.output_label || ''); - $('#conn-medium-type').val(conn.medium_type || ''); - $('#conn-medium-spec').val(conn.medium_spec || ''); $('#conn-medium-length').val(conn.medium_length || ''); - setSideButton(conn.is_top ? 'top' : 'bottom'); + + // Medium-Typen laden und Select befüllen + loadMediumTypes().then(() => { + renderMediumTypeSelect(conn.medium_type || ''); + // Spezifikation Select befüllen basierend auf gewähltem Typ + handleMediumTypeChange(); + // Gespeicherte Spezifikation setzen + if (conn.medium_spec) { + $('#conn-medium-spec').val(conn.medium_spec); + } + }); + + // Terminal-Position aus gespeicherter Verbindung + const connIsTop = conn.is_top || (conn.target_terminal_id === 't1'); + setSideButton(connIsTop ? 'top' : 'bottom'); } + + // Side-Buttons immer zeigen (Automaten haben keine feste Richtung) + $('#conn-side-fields').show(); + // Medium-Felder nur bei Abgang zeigen + $('#conn-output-fields').toggle(direction === 'output'); + + // Bundle-Option: Nur bei Abgang + Equipment mit mehr als 1 Terminal + const eq = App.equipment ? App.equipment.find(e => e.id == eqId) : null; + const type = eq ? App.equipmentTypes.find(t => t.id == eq.fk_equipment_type) : null; + const connIsTop = conn && (conn.is_top || conn.target_terminal_id === 't1'); + const termCount = getTerminalCount(type, connIsTop ? 'top' : 'bottom', parseFloat(eq?.width_te) || 1); + if (direction === 'output' && termCount > 1) { + $('#conn-bundle-fields').removeClass('hidden'); + $('#conn-bundle-all').prop('checked', conn && conn.bundled_terminals === 'all'); + } else { + $('#conn-bundle-fields').addClass('hidden'); + } + + openModal('connection'); } else { - // Neu anlegen - App.editConnectionId = null; - $('#connection-modal-title').text(direction === 'input' ? 'Einspeisung' : 'Abgang'); - $('#btn-delete-connection').addClass('hidden'); - $('#conn-color').val('#3498db'); - $('#conn-label').val(''); - $('#conn-medium-type').val(''); - $('#conn-medium-spec').val(''); - $('#conn-medium-length').val(''); - setSideButton('bottom'); + // Leerer Terminal -> Kontextmenü anzeigen + showTerminalContextMenu(e, eqId, terminalPosition); + } + } + + /** + * Kontextmenü für leere Terminals: Wahl zwischen Anschlusspunkt und Abgang + */ + function showTerminalContextMenu(e, eqId, terminalPosition) { + // Altes Menü entfernen + $('.terminal-context-menu').remove(); + + const x = e.touches ? e.touches[0].clientX : e.clientX; + const y = e.touches ? e.touches[0].clientY : e.clientY; + + const html = ` +
+
+ + Anschlusspunkt (L1/L2/L3) +
+
+ + Abgang (Verbraucher) +
+
+ `; + + $('body').append(html); + + // Click Handler + $('.tcm-item').on('click', function() { + const direction = $(this).data('type'); + $('.terminal-context-menu').remove(); + openConnectionDialog(eqId, direction, terminalPosition); + }); + + // Schließen bei Klick außerhalb + setTimeout(() => { + $(document).one('click', () => $('.terminal-context-menu').remove()); + }, 10); + } + + /** + * Medium-Typen (Kabeltypen) aus DB laden und cachen + */ + async function loadMediumTypes() { + if (App.mediumTypes) return App.mediumTypes; + + try { + // Nutze pwa_api.php für Token-basierte Authentifizierung + const response = await apiCall('ajax/pwa_api.php', { + action: 'get_medium_types' + }); + console.log('[PWA] loadMediumTypes response:', response); + + if (response.success && response.groups) { + App.mediumTypes = response.groups; + // Cache für Offline + localStorage.setItem('kundenkarte_medium_types', JSON.stringify(response.groups)); + return response.groups; + } else { + console.warn('[PWA] loadMediumTypes: no groups in response'); + } + } catch (err) { + console.error('[PWA] loadMediumTypes error:', err); + // Fallback auf Cache + const cached = localStorage.getItem('kundenkarte_medium_types'); + if (cached) { + App.mediumTypes = JSON.parse(cached); + return App.mediumTypes; + } } + // Fallback auf statische Liste + return null; + } + + /** + * Medium-Type Select befüllen + */ + function renderMediumTypeSelect(selectedValue) { + const groups = App.mediumTypes; + let html = ''; + + if (groups && groups.length > 0) { + groups.forEach(group => { + html += ``; + group.types.forEach(t => { + const selected = (selectedValue === t.ref) ? ' selected' : ''; + const specs = t.available_specs ? ` data-specs='${JSON.stringify(t.available_specs)}'` : ''; + const defSpec = t.default_spec ? ` data-default="${escapeHtml(t.default_spec)}"` : ''; + html += ``; + }); + html += ''; + }); + } else { + // Fallback auf statische Liste + ['NYM-J', 'NYY-J', 'H07V-K', 'CAT6', 'CAT7'].forEach(t => { + const selected = (selectedValue === t) ? ' selected' : ''; + html += ``; + }); + } + + $('#conn-medium-type').html(html); + } + + /** + * Medium-Type Change Handler - Spezifikationen laden + */ + function handleMediumTypeChange() { + const $option = $('#conn-medium-type option:selected'); + const specs = $option.data('specs'); + const defaultSpec = $option.data('default'); + + let html = ''; + + if (specs && specs.length > 0) { + specs.forEach(spec => { + const selected = (spec === defaultSpec) ? ' selected' : ''; + html += ``; + }); + } else { + html = ''; + } + + $('#conn-medium-spec').html(html); + } + + /** + * Verbindungs-Dialog öffnen (nach Auswahl Input/Output) + */ + async function openConnectionDialog(eqId, direction, terminalPosition) { + App.connectionEquipmentId = eqId; + App.connectionDirection = direction; + App.connectionTerminalPosition = terminalPosition; + App.editConnectionId = null; + + renderTypeSelect(direction, ''); + $('#connection-modal-title').text(direction === 'input' ? 'Anschlusspunkt' : 'Abgang'); + $('#btn-delete-connection').addClass('hidden'); + $('#conn-color').val('#3498db'); + $('#conn-label').val(''); + $('#conn-medium-length').val(''); + + // Medium-Typen laden und Select befüllen + await loadMediumTypes(); + renderMediumTypeSelect(''); + $('#conn-medium-spec').html(''); + + // Side-Button auf aktuelle Terminal-Position setzen + setSideButton(terminalPosition || 'bottom'); + + // Side-Buttons immer zeigen (Automaten haben keine feste Richtung) + $('#conn-side-fields').show(); // Medium-Felder nur bei Abgang zeigen $('#conn-output-fields').toggle(direction === 'output'); + // Bundle-Option: Nur bei Abgang + Equipment mit mehr als 1 Terminal + const eq = App.equipment ? App.equipment.find(e => e.id == eqId) : null; + const type = eq ? App.equipmentTypes.find(t => t.id == eq.fk_equipment_type) : null; + const termCount = getTerminalCount(type, terminalPosition || 'bottom', parseFloat(eq?.width_te) || 1); + if (direction === 'output' && termCount > 1) { + $('#conn-bundle-fields').removeClass('hidden'); + $('#conn-bundle-all').prop('checked', false); + } else { + $('#conn-bundle-fields').addClass('hidden'); + } + openModal('connection'); } @@ -1821,10 +2612,19 @@ const mediumType = isOutput ? ($('#conn-medium-type').val().trim() || '') : ''; const mediumSpec = isOutput ? ($('#conn-medium-spec').val().trim() || '') : ''; const mediumLength = isOutput ? ($('#conn-medium-length').val().trim() || '') : ''; - const isTop = isOutput && getSelectedSide() === 'top'; - // source_terminal_id wie Website: t1=oben, t2=unten - const sourceTerminalId = isOutput ? (isTop ? 't1' : 't2') : ''; + const bundledTerminals = isOutput && $('#conn-bundle-all').is(':checked') ? 'all' : ''; + + // Terminal-Position: t1=oben, t2=unten (gilt für Input UND Output) + // Bei Bearbeitung: Side-Button-Auswahl verwenden, sonst die ursprüngliche Position + const terminalPosition = App.editConnectionId ? getSelectedSide() : (App.connectionTerminalPosition || 'bottom'); + const isTop = terminalPosition === 'top'; + const terminalId = isTop ? 't1' : 't2'; + + // Für Output: source_terminal + const sourceTerminalId = isOutput ? terminalId : ''; const sourceTerminal = isOutput ? (isTop ? 'top' : 'output') : ''; + // Für Input: target_terminal + const targetTerminalId = !isOutput ? terminalId : ''; closeModal('connection'); @@ -1840,7 +2640,8 @@ medium_spec: mediumSpec, medium_length: mediumLength, source_terminal: sourceTerminal, - source_terminal_id: sourceTerminalId + source_terminal_id: sourceTerminalId, + bundled_terminals: bundledTerminals }; const updateLocal = (conn) => { @@ -1851,6 +2652,7 @@ conn.medium_type = mediumType; conn.medium_spec = mediumSpec; conn.medium_length = mediumLength; + conn.bundled_terminals = bundledTerminals; if (isOutput) { conn.is_top = isTop; conn.source_terminal_id = sourceTerminalId; @@ -1892,7 +2694,9 @@ medium_spec: mediumSpec, medium_length: mediumLength, source_terminal: sourceTerminal, - source_terminal_id: sourceTerminalId + source_terminal_id: sourceTerminalId, + target_terminal_id: targetTerminalId, + bundled_terminals: bundledTerminals }; const newConnBase = { @@ -1902,8 +2706,10 @@ medium_type: mediumType, medium_spec: mediumSpec, medium_length: mediumLength, + bundled_terminals: bundledTerminals, is_top: isTop, - source_terminal_id: sourceTerminalId + source_terminal_id: sourceTerminalId, + target_terminal_id: targetTerminalId }; if (App.isOnline) { @@ -2115,11 +2921,12 @@ } function showOfflineBar() { - $('#offline-indicator').removeClass('hidden'); + // Status-Indikator entfernt - nur Toast-Nachricht + showToast('Offline', 'warning'); } function hideOfflineBar() { - $('#offline-indicator').addClass('hidden'); + // Status-Indikator entfernt } function escapeHtml(text) { diff --git a/pwa.php b/pwa.php index 9cb6910..b9b5c15 100644 --- a/pwa.php +++ b/pwa.php @@ -44,7 +44,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db'); - + @@ -98,8 +98,12 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
- @@ -177,6 +181,15 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
+ +
+
+ + +
+
+ +
+ +
+ + +
+
+ + +
- -
- - -
+ +
- - -
-
- - + +
@@ -346,6 +374,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db'); window.DOLIBARR_URL = ''; window.MODULE_URL = '/custom/kundenkarte'; - + diff --git a/sw.js b/sw.js index 2979c8f..e1dba82 100644 --- a/sw.js +++ b/sw.js @@ -3,19 +3,20 @@ * Offline-First für Schaltschrank-Dokumentation */ -const CACHE_NAME = 'kundenkarte-pwa-v2.9'; -const OFFLINE_CACHE = 'kundenkarte-offline-v2.9'; +const CACHE_NAME = 'kundenkarte-pwa-v5.2'; +const OFFLINE_CACHE = 'kundenkarte-offline-v5.2'; -// Statische Assets die immer gecached werden +// Statische Assets die immer gecached werden (ohne Query-String) const STATIC_ASSETS = [ 'pwa.php', - 'css/pwa.css', - 'js/pwa.js', 'img/pwa-icon-192.png', 'img/pwa-icon-512.png', '../../../includes/jquery/js/jquery.min.js' ]; +// Assets mit Versions-Query-String - NICHT cachen, immer vom Netzwerk laden +const VERSIONED_ASSETS = ['pwa.css', 'pwa.js']; + // Install - Cache statische Assets self.addEventListener('install', event => { console.log('[SW] Installing...'); @@ -55,6 +56,15 @@ self.addEventListener('fetch', event => { return; } + // Versionierte Assets (CSS/JS mit ?v=X) - IMMER Netzwerk, kein Cache + // Damit neue Versionen sofort geladen werden + if (url.search && VERSIONED_ASSETS.some(a => url.pathname.includes(a))) { + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); + return; + } + // AJAX Requests - Netzwerk mit Offline-Fallback if (url.pathname.includes('/ajax/')) { event.respondWith(
'.$langs->trans("PWAMobileApp").'