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").' | '; 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 += ` +