diff --git a/CLAUDE.md b/CLAUDE.md index 31d6e58..5484a65 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,7 +135,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat - `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA - `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter) - `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen -- `sw.js` - Service Worker für Offline-Cache (v1.8) +- `sw.js` - Service Worker für Offline-Cache (v2.7) - `manifest.json` - Web App Manifest für Installation ### Workflow @@ -154,28 +154,35 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat - Header: sticky, primary-Farbe ### Kontakt-Adressen -- `get_anlagen` API liefert jetzt `anlagen` (Kunden-Ebene) + `contacts` (Adressen) -- Kontakt-Gruppen werden als aufklappbare Akkordeons dargestellt +- `get_anlagen` API liefert `anlagen` (Kunden-Ebene) + `contacts` (Adressen) +- Kontakt-Gruppen (Gebäude/Standorte) werden OBEN als vertikale Liste dargestellt +- Chevron-Pfeil zeigt Aufklapp-Status, Anlagen werden bei Klick geladen +- Kunden-Anlagen darunter mit Kunden-Adresse als Trennlabel - `get_contact_anlagen` API lädt Anlagen pro Kontakt-Adresse bei Bedarf -- Kontakt-Adressen zeigen Name, Adresse, Ort und Anzahl Anlagen +- Layout: Alles untereinander (Handy-optimiert), keine Grid-Spalten ### Schaltplan-Editor - Equipment-Blöcke als CSS Grid (`grid-template-columns: repeat(totalTE, 1fr)`) - Blöcke positioniert per `grid-column` basierend auf `position_te` und `width_te` +- Equipment-Blöcke: 80px Höhe, Sicherungsautomat-Optik mit Border und Schatten - `block_label` und `block_color` aus Backend (wie Website) - Abgang-Labels (Outputs) werden über/unter den Automaten angezeigt -- Toggle-Button zum Wechseln der Label-Position (oben/unten), in localStorage gespeichert - Connections mit `fk_target IS NULL` = Ausgänge/Abgänge -- Quick-Select erweitert: LS, FI/RCD, AFDD, FI/LS-Kombi -- Intelligente Positionsberechnung mit Lücken-Erkennung -- Hutschiene zeigt belegt/gesamt TE, +-Button wird disabled wenn voll +- Intelligente Positionsberechnung mit Lücken-Erkennung (getMaxGap) +- +-Button disabled wenn Hutschiene voll, Typ-Buttons gefiltert nach verfügbarer Breite +- Hutschiene zeigt belegt/gesamt TE ### Navigation & State - Browser-History Support (hardware Zurück-Button funktioniert) - Session-State wird in `sessionStorage` gespeichert (Screen, Kunde, Anlage) - Bei Seiten-Refresh wird letzter Zustand wiederhergestellt +- Popstate-Handler: Lädt Anlagen-Daten nach falls Liste leer (z.B. nach Refresh→Back) - Sync-Button lädt jetzt erst Offline-Queue, dann Daten neu +### Anlagen-Übersicht +- Anlagen-Cards: Horizontales Layout (Icon links, Titel rechts), volle Breite +- Einspaltiges vertikales Layout - optimiert fürs Handy + ### Token-Authentifizierung - Tokens enthalten: user_id, login, created, expires, hash - Hash = MD5(user_id + login + MAIN_SECURITY_SALT) diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index ee91201..678403a 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -78,6 +78,7 @@ require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php'; require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php'; +require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/mediumtype.class.php'; $action = GETPOST('action', 'aZ09'); @@ -128,18 +129,43 @@ switch ($action) { break; } + // Feld-Metadaten laden (show_in_tree Felder für Anzeige) + $fieldMeta = array(); + $sqlFields = "SELECT fk_anlage_type, field_code, field_label, tree_display_mode, badge_color, show_in_tree, field_type"; + $sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field"; + $sqlFields .= " WHERE active = 1 AND show_in_tree = 1"; + $sqlFields .= " ORDER BY position"; + $resFields = $db->query($sqlFields); + if ($resFields) { + while ($fObj = $db->fetch_object($resFields)) { + if ($fObj->field_type === 'header') continue; + $fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array( + 'label' => $fObj->field_label, + 'display' => $fObj->tree_display_mode ?: 'badge', + 'color' => $fObj->badge_color ?: '', + ); + } + $db->free($resFields); + } + // Root-Anlagen ohne Kontaktzuweisung (Kunden-Ebene) $anlage = new Anlage($db); $anlagen = $anlage->fetchChildren(0, $customerId); $result = array(); foreach ($anlagen as $a) { - $result[] = array( + $item = array( 'id' => $a->id, 'label' => $a->label, 'type' => $a->type_label, 'has_editor' => !empty($a->schematic_editor_enabled) ); + // Feld-Badges hinzufügen + $fields = pwaGetAnlageFields($a, $fieldMeta); + if (!empty($fields)) { + $item['fields'] = $fields; + } + $result[] = $item; } // Kontakt-Adressen mit Anlagen laden @@ -181,17 +207,43 @@ switch ($action) { break; } + // Feld-Metadaten laden falls nicht schon vorhanden + if (!isset($fieldMeta)) { + $fieldMeta = array(); + $sqlFields = "SELECT fk_anlage_type, field_code, field_label, tree_display_mode, badge_color, show_in_tree, field_type"; + $sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field"; + $sqlFields .= " WHERE active = 1 AND show_in_tree = 1"; + $sqlFields .= " ORDER BY position"; + $resFields = $db->query($sqlFields); + if ($resFields) { + while ($fObj = $db->fetch_object($resFields)) { + if ($fObj->field_type === 'header') continue; + $fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array( + 'label' => $fObj->field_label, + 'display' => $fObj->tree_display_mode ?: 'badge', + 'color' => $fObj->badge_color ?: '', + ); + } + $db->free($resFields); + } + } + $anlage = new Anlage($db); $anlagen = $anlage->fetchChildrenByContact(0, $customerId, $contactId); $result = array(); foreach ($anlagen as $a) { - $result[] = array( + $item = array( 'id' => $a->id, 'label' => $a->label, 'type' => $a->type_label, 'has_editor' => !empty($a->schematic_editor_enabled) ); + $fields = pwaGetAnlageFields($a, $fieldMeta); + if (!empty($fields)) { + $item['fields'] = $fields; + } + $result[] = $item; } $response['success'] = true; @@ -256,11 +308,15 @@ switch ($action) { } } + // Equipment-Typen laden (benötigt für Terminal-Position-Auflösung) + $eqType = new EquipmentType($db); + $types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive + // Abgänge laden (Connections mit fk_target IS NULL = Ausgänge) $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, connection_type"; + $sql = "SELECT rowid, fk_source, output_label, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id"; $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; $sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")"; $sql .= " AND fk_target IS NULL"; @@ -268,21 +324,86 @@ switch ($action) { $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { + // Position bestimmen wie Website: source_terminal_id → Terminal-Config + $isTop = false; + if ($obj->source_terminal === 'top') { + // PWA-erstellte Verbindung mit expliziter Top-Angabe + $isTop = true; + } elseif (!empty($obj->source_terminal_id)) { + // Website-erstellte Verbindung: Terminal-Position aus Equipment-Typ ermitteln + $termId = $obj->source_terminal_id; + $eqTypeId = null; + foreach ($equipmentData as $e) { + if ($e['id'] == $obj->fk_source) { + $eqTypeId = $e['fk_equipment_type']; + break; + } + } + if ($eqTypeId) { + // Terminal-Config des Typs prüfen + $termResolved = false; + foreach ($types as $t) { + if ($t->id == $eqTypeId && !empty($t->terminals_config)) { + $config = json_decode($t->terminals_config, true); + $terms = $config['terminals'] ?? ($config['inputs'] ?? []); + if (!empty($config['terminals'])) { + foreach ($config['terminals'] as $term) { + if ($term['id'] === $termId) { + $isTop = ($term['pos'] === 'top'); + $termResolved = true; + break; + } + } + } + break; + } + } + // Fallback: t1=oben, t2=unten (Standard LS) + if (!$termResolved) { + $isTop = ($termId === 't1'); + } + } + } + $outputsData[] = array( 'id' => $obj->rowid, 'fk_source' => $obj->fk_source, 'output_label' => $obj->output_label, 'medium_type' => $obj->medium_type, 'medium_spec' => $obj->medium_spec, - 'connection_type' => $obj->connection_type + 'medium_length' => $obj->medium_length, + 'connection_type' => $obj->connection_type, + 'color' => $obj->color, + 'source_terminal_id' => $obj->source_terminal_id ?: '', + 'is_top' => $isTop ); } } } - // Load equipment types - $eqType = new EquipmentType($db); - $types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive + // Einspeisungen laden (Connections mit fk_source IS NULL = Inputs) + $inputsData = array(); + if (!empty($equipmentData)) { + $sql = "SELECT rowid, fk_target, output_label, connection_type, color"; + $sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; + $sql .= " WHERE fk_target IN (".implode(',', $equipmentIds).")"; + $sql .= " AND fk_source IS NULL"; + $sql .= " AND status = 1"; + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $inputsData[] = array( + 'id' => $obj->rowid, + 'fk_target' => $obj->fk_target, + 'output_label' => $obj->output_label, + 'connection_type' => $obj->connection_type, + 'color' => $obj->color + ); + } + } + } + + // Equipment-Typen für Response aufbereiten (bereits oben geladen) $typesData = array(); foreach ($types as $t) { $typesData[] = array( @@ -300,6 +421,7 @@ switch ($action) { $response['carriers'] = $carriersData; $response['equipment'] = $equipmentData; $response['outputs'] = $outputsData; + $response['inputs'] = $inputsData; $response['types'] = $typesData; break; @@ -351,7 +473,15 @@ switch ($action) { break; } + // Anlage-ID vom Panel holen (benötigt für Carrier-Erstellung) + $panelObj = new EquipmentPanel($db); + if ($panelObj->fetch($panelId) <= 0) { + $response['error'] = 'Panel nicht gefunden'; + break; + } + $carrier = new EquipmentCarrier($db); + $carrier->fk_anlage = $panelObj->fk_anlage; $carrier->fk_panel = $panelId; $carrier->label = $label ?: 'Hutschiene'; $carrier->total_te = $totalTe; @@ -365,6 +495,70 @@ switch ($action) { } break; + // ============================================ + // UPDATE CARRIER + // ============================================ + case 'update_carrier': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $carrierId = GETPOSTINT('carrier_id'); + if ($carrierId <= 0) { + $response['error'] = 'Keine Carrier-ID'; + break; + } + + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($carrierId) <= 0) { + $response['error'] = 'Hutschiene nicht gefunden'; + break; + } + + $carrier->label = GETPOST('label', 'alphanohtml') ?: $carrier->label; + $newTe = GETPOSTINT('total_te'); + if ($newTe > 0) { + $carrier->total_te = $newTe; + } + + $result = $carrier->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $carrier->error ?: 'Fehler beim Aktualisieren'; + } + break; + + // ============================================ + // DELETE CARRIER + // ============================================ + case 'delete_carrier': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $carrierId = GETPOSTINT('carrier_id'); + if ($carrierId <= 0) { + $response['error'] = 'Keine Carrier-ID'; + break; + } + + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($carrierId) <= 0) { + $response['error'] = 'Hutschiene nicht gefunden'; + break; + } + + $result = $carrier->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $carrier->error ?: 'Fehler beim Löschen'; + } + break; + // ============================================ // CREATE EQUIPMENT // ============================================ @@ -397,18 +591,343 @@ switch ($action) { $equipment->width_te = $eqType->width_te ?: 1; $equipment->field_values = $fieldValues; + // Bezeichnung automatisch generieren wenn leer (wie Website) + if (empty(trim($equipment->label ?? ''))) { + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($carrierId) > 0) { + $carrierLabel = $carrier->label ?: ('R'.$carrier->id); + $posStart = $equipment->position_te; + $posEnd = $posStart + $equipment->width_te - 1; + if ($equipment->width_te > 1) { + $equipment->label = $carrierLabel.'.'.$posStart.'-'.$posEnd; + } else { + $equipment->label = $carrierLabel.'.'.$posStart; + } + } + } + $result = $equipment->create($user); if ($result > 0) { $response['success'] = true; $response['equipment_id'] = $result; + $response['label'] = $equipment->label; + $response['block_label'] = $equipment->getBlockLabel(); + $response['block_color'] = $equipment->getBlockColor(); } else { $response['error'] = $equipment->error ?: 'Fehler beim Anlegen'; } break; + // ============================================ + // GET TYPE FIELDS (Felder pro Equipment-Typ) + // ============================================ + case 'get_type_fields': + $typeId = GETPOSTINT('type_id'); + $equipmentId = GETPOSTINT('equipment_id'); + + if ($typeId <= 0) { + $response['error'] = 'Keine Typ-ID'; + break; + } + + $type = new EquipmentType($db); + if ($type->fetch($typeId) <= 0) { + $response['error'] = 'Typ nicht gefunden'; + break; + } + + $fields = $type->fetchFields(1); + + // Bestehende Werte laden (bei Bearbeitung) + $existingValues = array(); + if ($equipmentId > 0) { + $eq = new Equipment($db); + if ($eq->fetch($equipmentId) > 0) { + $existingValues = $eq->getFieldValues(); + } + } + + $result = array(); + foreach ($fields as $field) { + $value = isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : $field->field_default; + $result[] = array( + 'code' => $field->field_code, + 'label' => $field->field_label, + 'type' => $field->field_type, + 'options' => $field->field_options, + 'required' => (int) $field->required, + 'show_on_block' => (int) $field->show_on_block, + 'value' => $value + ); + } + + $response['success'] = true; + $response['fields'] = $result; + break; + + // ============================================ + // UPDATE EQUIPMENT + // ============================================ + case 'update_equipment': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $equipmentId = GETPOSTINT('equipment_id'); + $label = GETPOST('label', 'alphanohtml'); + $fieldValues = GETPOST('field_values', 'nohtml'); + + if ($equipmentId <= 0) { + $response['error'] = 'Keine Equipment-ID'; + break; + } + + $equipment = new Equipment($db); + if ($equipment->fetch($equipmentId) <= 0) { + $response['error'] = 'Automat nicht gefunden'; + break; + } + + $equipment->label = $label; + $equipment->field_values = $fieldValues; + + $result = $equipment->update($user); + if ($result > 0) { + $response['success'] = true; + // Aktualisierte Daten zurückgeben + $response['block_label'] = $equipment->getBlockLabel(); + $response['block_color'] = $equipment->getBlockColor(); + } else { + $response['error'] = $equipment->error ?: 'Fehler beim Aktualisieren'; + } + break; + + // ============================================ + // DELETE EQUIPMENT + // ============================================ + case 'delete_equipment': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $equipmentId = GETPOSTINT('equipment_id'); + + if ($equipmentId <= 0) { + $response['error'] = 'Keine Equipment-ID'; + break; + } + + $equipment = new Equipment($db); + if ($equipment->fetch($equipmentId) <= 0) { + $response['error'] = 'Automat nicht gefunden'; + break; + } + + $result = $equipment->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $equipment->error ?: 'Fehler beim Löschen'; + } + break; + + // ============================================ + // CREATE CONNECTION (Abgang oder Einspeisung) + // ============================================ + case 'create_connection': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $equipmentId = GETPOSTINT('equipment_id'); + $direction = GETPOST('direction', 'alpha'); // 'output' oder 'input' + $connectionType = GETPOST('connection_type', 'alphanohtml'); + $outputLabel = GETPOST('output_label', 'alphanohtml'); + $mediumType = GETPOST('medium_type', 'alphanohtml'); + $mediumSpec = GETPOST('medium_spec', 'alphanohtml'); + $mediumLength = GETPOST('medium_length', 'alphanohtml'); + $sourceTerminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output'; + $sourceTerminalId = GETPOST('source_terminal_id', 'alphanohtml'); + + if ($equipmentId <= 0) { + $response['error'] = 'Keine Equipment-ID'; + break; + } + + // Equipment prüfen und Carrier-ID ermitteln + $eq = new Equipment($db); + if ($eq->fetch($equipmentId) <= 0) { + $response['error'] = 'Automat nicht gefunden'; + break; + } + + $conn = new EquipmentConnection($db); + $conn->connection_type = $connectionType; + $conn->color = GETPOST('color', 'alphanohtml'); + $conn->output_label = $outputLabel; + $conn->fk_carrier = $eq->fk_carrier; + + if ($direction === 'input') { + // Einspeisung: fk_source = NULL, fk_target = Equipment + $conn->fk_target = $equipmentId; + $conn->fk_source = null; + $conn->target_terminal = 'input'; + } else { + // Abgang: fk_source = Equipment, fk_target = NULL + $conn->fk_source = $equipmentId; + $conn->fk_target = null; + $conn->source_terminal = $sourceTerminal; + $conn->source_terminal_id = $sourceTerminalId ?: ($sourceTerminal === 'top' ? 't1' : 't2'); + $conn->medium_type = $mediumType; + $conn->medium_spec = $mediumSpec; + $conn->medium_length = $mediumLength; + } + + $result = $conn->create($user); + if ($result > 0) { + $response['success'] = true; + $response['connection_id'] = $result; + } else { + $response['error'] = $conn->error ?: 'Fehler beim Anlegen'; + } + break; + + // ============================================ + // UPDATE CONNECTION + // ============================================ + case 'update_connection': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $connectionId = GETPOSTINT('connection_id'); + if ($connectionId <= 0) { + $response['error'] = 'Keine Verbindungs-ID'; + break; + } + + $conn = new EquipmentConnection($db); + if ($conn->fetch($connectionId) <= 0) { + $response['error'] = 'Verbindung nicht gefunden'; + break; + } + + $conn->connection_type = GETPOST('connection_type', 'alphanohtml'); + $conn->color = GETPOST('color', 'alphanohtml'); + $conn->output_label = GETPOST('output_label', 'alphanohtml'); + $conn->medium_type = GETPOST('medium_type', 'alphanohtml'); + $conn->medium_spec = GETPOST('medium_spec', 'alphanohtml'); + $conn->medium_length = GETPOST('medium_length', 'alphanohtml'); + if (GETPOSTISSET('source_terminal')) { + $conn->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: $conn->source_terminal; + } + if (GETPOSTISSET('source_terminal_id')) { + $conn->source_terminal_id = GETPOST('source_terminal_id', 'alphanohtml') ?: $conn->source_terminal_id; + } + + $result = $conn->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $conn->error ?: 'Fehler beim Aktualisieren'; + } + break; + + // ============================================ + // DELETE CONNECTION + // ============================================ + case 'delete_connection': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $connectionId = GETPOSTINT('connection_id'); + if ($connectionId <= 0) { + $response['error'] = 'Keine Verbindungs-ID'; + break; + } + + $conn = new EquipmentConnection($db); + if ($conn->fetch($connectionId) <= 0) { + $response['error'] = 'Verbindung nicht gefunden'; + break; + } + + $result = $conn->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $conn->error ?: 'Fehler beim Löschen'; + } + break; + + // ============================================ + // GET MEDIUM TYPES (Kabeltypen aus DB) + // ============================================ + case 'get_medium_types': + $mediumType = new MediumType($db); + $grouped = $mediumType->fetchGroupedByCategory(1); // System 1 = Elektro + + $result = array(); + foreach ($grouped as $category => $types) { + $catTypes = array(); + foreach ($types as $t) { + $catTypes[] = array( + 'ref' => $t->ref, + 'label' => $t->label, + 'default_spec' => $t->default_spec, + 'available_specs' => $t->getAvailableSpecsArray(), + ); + } + $result[] = array( + 'category' => $category, + 'category_label' => $types[0]->getCategoryLabel(), + 'types' => $catTypes + ); + } + + $response['success'] = true; + $response['groups'] = $result; + break; + default: $response['error'] = 'Unbekannte Aktion: ' . $action; } echo json_encode($response); $db->close(); + +/** + * Feld-Badges für eine Anlage aufbereiten (show_in_tree Felder) + * + * @param Anlage $anlage Anlage-Objekt + * @param array $fieldMeta Feld-Metadaten [typeId][code] = {label, display, color} + * @return array Array von Feld-Objekten mit label, value, color + */ +function pwaGetAnlageFields($anlage, $fieldMeta) { + $result = array(); + $values = $anlage->getFieldValues(); + if (empty($values)) return $result; + + $typeId = (int) $anlage->fk_anlage_type; + $meta = isset($fieldMeta[$typeId]) ? $fieldMeta[$typeId] : array(); + if (empty($meta)) return $result; + + foreach ($meta as $code => $fm) { + $val = isset($values[$code]) ? $values[$code] : null; + if ($val === '' || $val === null) continue; + + $result[] = array( + 'label' => $fm['label'], + 'value' => $val, + 'color' => $fm['color'], + 'display' => $fm['display'], + ); + } + return $result; +} diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php index d1e7365..b174cf3 100755 --- a/class/equipmentconnection.class.php +++ b/class/equipmentconnection.class.php @@ -218,8 +218,10 @@ class EquipmentConnection extends CommonObject $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; $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 .= ", 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"); $sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL"); $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL"); diff --git a/css/pwa.css b/css/pwa.css index d809db9..459a105 100644 --- a/css/pwa.css +++ b/css/pwa.css @@ -443,25 +443,27 @@ body { .anlagen-grid { display: flex; - flex-wrap: wrap; - gap: 10px; - padding: 16px; + flex-direction: column; + gap: 6px; + padding: 12px; } -.anlagen-grid > .anlage-card { - width: calc(50% - 5px); - box-sizing: border-box; -} - -.anlagen-grid > .contact-group { - width: 100%; +/* Trennlabel Kunden-Adresse */ +.anlagen-section-label { + font-size: 12px; + font-weight: 600; + color: var(--colortextmuted); + padding: 8px 4px 2px; + border-top: 1px solid var(--colorborder); + margin-top: 4px; } .anlage-card { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - padding: 18px 14px; + gap: 12px; + padding: 14px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 8px; @@ -475,48 +477,84 @@ body { } .anlage-card-icon { - width: 52px; - height: 52px; + width: 44px; + height: 44px; background: var(--success); - border-radius: 12px; + border-radius: 10px; display: flex; align-items: center; justify-content: center; - margin-bottom: 10px; + flex-shrink: 0; } .anlage-card-icon svg { - width: 28px; - height: 28px; + width: 24px; + height: 24px; fill: #fff; } .anlage-card-title { - font-size: 14px; + font-size: 15px; font-weight: 600; - text-align: center; word-break: break-word; + line-height: 1.3; +} + +.anlage-card-content { + flex: 1; + min-width: 0; } .anlage-card-type { - font-size: 11px; + font-size: 12px; color: var(--colortextmuted); - margin-top: 4px; + margin-top: 2px; +} + +.anlage-card-arrow { + width: 20px; + height: 20px; + fill: var(--colortextmuted); + flex-shrink: 0; +} + +.anlage-card-fields { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.anlage-field-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 4px; + background: var(--colorbackinput); + color: #fff; + white-space: nowrap; } /* ============================================ CONTACT GROUPS ============================================ */ +.contact-list { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + .contact-group { - margin-bottom: 8px; + margin-bottom: 0; } .contact-group-header { display: flex; align-items: center; - gap: 12px; - padding: 14px 16px; + gap: 10px; + padding: 12px 14px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 8px; @@ -552,8 +590,12 @@ body { margin-top: 2px; } +.contact-group-info { + flex: 1; + min-width: 0; +} + .contact-group-count { - margin-left: auto; background: var(--colorbackinput); color: var(--colortextmuted); font-size: 12px; @@ -562,35 +604,35 @@ body { border-radius: 10px; min-width: 24px; text-align: center; + flex-shrink: 0; +} + +.contact-group-chevron { + width: 20px; + height: 20px; + fill: var(--colortextmuted); + flex-shrink: 0; + transition: transform 0.2s; +} + +.contact-group.expanded .contact-group-chevron { + transform: rotate(90deg); } .contact-anlagen-list { - padding: 10px; + padding: 8px; background: rgba(59, 60, 62, 0.4); border: 1px solid var(--colorborder); border-top: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; - flex-wrap: wrap; - gap: 8px; + flex-direction: column; + gap: 6px; display: none; } .contact-group.expanded .contact-anlagen-list { display: flex; - gap: 8px; -} - -.contact-anlagen-list .anlage-card { - width: calc(50% - 4px); - padding: 14px 10px; - box-sizing: border-box; -} - -.contact-anlagen-list .anlage-card-icon { - width: 44px; - height: 44px; - margin-bottom: 8px; } .contact-anlagen-list .loading-container, @@ -625,8 +667,9 @@ body { background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 8px; - margin-bottom: 12px; + margin: 0 auto 12px auto; overflow: hidden; + max-width: 900px; } .panel-header { @@ -667,6 +710,7 @@ body { justify-content: space-between; padding: 8px 10px; background: rgba(255,255,255,0.03); + cursor: pointer; } .carrier-label { @@ -681,35 +725,35 @@ body { opacity: 0.7; } -.carrier-body { - padding: 5px; - display: flex; - align-items: stretch; - gap: 3px; - min-height: 50px; -} - -.carrier-grid { - flex: 1; - min-width: 0; +/* Carrier-Content: EIN Grid für Terminals + Equipment + +-Button + Spalten: repeat(totalTE, 1fr) auto + Zeile 1: Input-Terminals (Einspeisungen) + Zeile 2: Equipment-Blöcke + +-Button + Zeile 3: Output-Terminals (Abgänge) */ +.carrier-content { display: grid; gap: 2px; + padding: 5px; + grid-template-rows: auto auto auto; + align-items: stretch; } -/* Equipment Block */ +/* Equipment Block (Zeile 2) - Sicherungsautomat-Optik */ .equipment-block { display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: space-between; min-width: 0; - height: 52px; - padding: 2px 1px; + height: 80px; + padding: 4px 2px; background: var(--primary); border-radius: 4px; cursor: pointer; transition: all 0.15s ease; overflow: hidden; + border: 1px solid rgba(255,255,255,0.15); + box-shadow: 0 1px 3px rgba(0,0,0,0.3); } .equipment-block:active { @@ -724,10 +768,10 @@ body { } .equipment-block-value { - font-size: 11px; + font-size: 13px; font-weight: bold; color: #fff; - line-height: 1.1; + line-height: 1.2; } .equipment-block-label { @@ -754,119 +798,103 @@ body { padding: 0 1px; } -/* Abgang-Labels (Zeile über/unter den Blöcken) */ -.carrier-labels { - display: grid; - gap: 2px; - padding: 0 5px; - min-height: 120px; -} - -.carrier-label-cell { - display: flex; - align-items: flex-end; - justify-content: center; - min-width: 0; - overflow: visible; -} - -/* Labels oben: Text vertikal von unten nach oben */ -.labels-top .carrier-labels { - align-items: end; -} - -.labels-top .carrier-label-text { - writing-mode: vertical-rl; - transform: rotate(180deg); - font-size: 11px; - font-weight: bold; - color: #fff; - line-height: 1.3; - max-height: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 4px 0; -} - -.labels-top .carrier-label-text .cable-info { - font-weight: normal; - font-size: 10px; - color: #888; -} - -/* Labels unten: Text vertikal von oben nach unten */ -.labels-bottom .carrier-labels { - align-items: start; -} - -.labels-bottom .carrier-label-cell { - align-items: flex-start; -} - -.labels-bottom .carrier-label-text { - writing-mode: vertical-rl; - font-size: 11px; - font-weight: bold; - color: #fff; - line-height: 1.3; - max-height: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 4px 0; -} - -.labels-bottom .carrier-label-text .cable-info { - font-weight: normal; - font-size: 10px; - color: #888; -} - -/* Toggle-Button für Label-Position */ -.btn-toggle-labels { - width: 44px; - height: 44px; +/* Terminal-Point = Einzelne klickbare Klemme + Sitzt in derselben Grid-Spalte wie der zugehörige Equipment-Block */ +.terminal-point { display: flex; + flex-direction: column; align-items: center; - justify-content: center; - background: rgba(255,255,255,0.15); - border: none; - border-radius: 8px; - color: #fff; + gap: 2px; cursor: pointer; - transition: background 0.2s, transform 0.3s; + padding: 3px 2px; + border-radius: 4px; + transition: background 0.15s; + min-width: 0; } -.btn-toggle-labels:active { - background: rgba(255,255,255,0.3); +.terminal-point.terminal-input { + align-self: end; } -.btn-toggle-labels svg { - width: 22px; - height: 22px; - fill: currentColor; +.terminal-point.terminal-output { + align-self: start; } -/* Pfeil nach oben = Labels oben */ -.btn-toggle-labels.labels-top svg { - transform: rotate(0deg); +.terminal-point:active { + background: rgba(255,255,255,0.15); } -/* Pfeil nach unten = Labels unten */ -.btn-toggle-labels.labels-bottom svg { +/* Terminal-Punkt (Kreis für Inputs) */ +.terminal-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + border: 2px solid rgba(255,255,255,0.5); +} + +.terminal-dot.terminal-empty { + background: transparent; + border: 2px dashed rgba(255,255,255,0.25); +} + +/* Terminal-Pfeil (für Outputs) */ +.terminal-arrow { + width: 0; + height: 0; + flex-shrink: 0; +} + +.terminal-arrow-down { + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-top: 10px solid var(--arrow-color, #888); +} + +.terminal-arrow-up { + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 10px solid var(--arrow-color, #888); +} + +/* Phase-Anzeige am Terminal */ +.terminal-phase { + font-size: 8px; + font-weight: bold; + color: rgba(255,255,255,0.7); + text-align: center; + line-height: 1; +} + +/* Abgang-Label (vertikal) */ +.terminal-label { + writing-mode: vertical-rl; transform: rotate(180deg); + font-size: 9px; + font-weight: bold; + color: #fff; + line-height: 1.1; + max-height: 90px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 2px 0; } -/* Add Button in Carrier */ +.terminal-label .cable-info { + font-weight: normal; + font-size: 8px; + color: #888; +} + +/* Output-Zeile braucht mehr Platz wenn Labels vorhanden */ +/* Add Button in Carrier (letzte Spalte, Zeile 2) */ .btn-add-equipment { display: flex; align-items: center; justify-content: center; width: 36px; min-width: 36px; - flex-shrink: 0; - align-self: stretch; padding: 4px; background: transparent; border: 2px dashed var(--colorborder); @@ -972,10 +1000,11 @@ body { bottom: 0; background: rgba(0,0,0,0.7); display: none; - align-items: flex-end; + align-items: center; justify-content: center; z-index: 1000; - padding: var(--safe-bottom); + padding: 16px; + padding-bottom: calc(16px + var(--safe-bottom)); } .modal.active { @@ -988,23 +1017,25 @@ body { max-height: 85vh; background: var(--colorbackline); border: 1px solid var(--colorborder); - border-radius: 12px 12px 0 0; + border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; - animation: slideUp 0.3s ease; + animation: modalFadeIn 0.25s ease; } .modal-small { max-height: auto; } -@keyframes slideUp { +@keyframes modalFadeIn { from { - transform: translateY(100%); + opacity: 0; + transform: scale(0.95); } to { - transform: translateY(0); + opacity: 1; + transform: scale(1); } } @@ -1051,21 +1082,101 @@ body { gap: 12px; padding: 16px 20px; border-top: 1px solid var(--colorborder); + justify-content: space-between; + align-items: center; } .modal-footer .btn { + flex: 0 1 auto; +} + +.modal-footer-right { + display: flex; + gap: 12px; flex: 1; } +.modal-footer-right .btn { + flex: 1; +} + +/* Wenn kein Delete-Button sichtbar: Footer-Right nimmt volle Breite */ +.modal-footer .btn-danger.hidden + .modal-footer-right { + width: 100%; +} + /* ============================================ TYPE GRID ============================================ */ -.step-values-area { - display: none; - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--colorborder); +/* Zurück-Button im Modal-Header */ +.btn-back-modal { + background: none; + border: none; + color: var(--colortext); + padding: 4px; + cursor: pointer; + flex-shrink: 0; +} + +.btn-back-modal svg { + width: 24px; + height: 24px; + fill: currentColor; +} + +/* Dynamische Felder */ +#eq-dynamic-fields .form-group { + margin-bottom: 14px; +} + +#eq-dynamic-fields .form-select { + width: 100%; + padding: 12px; + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); + font-size: 16px; + -webkit-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 24px; +} + +#eq-dynamic-fields .form-input { + width: 100%; + padding: 12px; + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); + font-size: 16px; + box-sizing: border-box; +} + +.field-required { + color: var(--danger); + font-weight: bold; +} + +.field-error { + border-color: var(--danger) !important; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 20px; + height: 20px; } .step-label { @@ -1100,6 +1211,12 @@ body { background: rgba(173, 140, 79, 0.15); } +.type-btn.disabled { + opacity: 0.3; + cursor: not-allowed; + pointer-events: none; +} + .type-btn-icon { font-size: 24px; margin-bottom: 6px; @@ -1135,31 +1252,124 @@ body { background: rgba(173, 140, 79, 0.15); } -/* Value Quick Select */ -.value-quick { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 12px; +/* Connection Modal - Phase Grid */ +.phase-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; } -.value-chip { - padding: 10px 16px; +.phase-btn { + padding: 10px 4px; background: var(--colorbackinput); - border: 1px solid var(--colorborder); - border-radius: 20px; + border: 2px solid var(--colorborder); + border-radius: 8px; color: var(--colortext); font-size: 14px; - font-weight: 500; + font-weight: 700; cursor: pointer; transition: all 0.2s; + text-align: center; } -.value-chip:active, -.value-chip.selected { - background: var(--butactionbg); +.phase-btn:active, +.phase-btn.selected { border-color: var(--butactionbg); - color: var(--textbutaction); + background: rgba(173, 140, 79, 0.15); +} + +/* Phasen-Farben für Buttons */ +.phase-btn[data-type="L1"].selected { border-color: #8B4513; background: rgba(139,69,19,0.2); } +.phase-btn[data-type="L2"].selected { border-color: #555; background: rgba(50,50,50,0.3); } +.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="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 */ +.side-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} + +.side-btn { + padding: 10px 4px; + background: var(--colorbackinput); + border: 2px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.side-btn:active, +.side-btn.selected { + border-color: var(--butactionbg); + background: rgba(173, 140, 79, 0.15); +} + +/* Connection Modal - Typ + Farbe nebeneinander */ +.form-row { + display: flex; + gap: 10px; + align-items: flex-end; +} + +.form-group-grow { + flex: 1; + min-width: 0; +} + +.form-group-color { + flex: 0 0 auto; + width: 60px; +} + +.form-color { + width: 100%; + height: 44px; + padding: 2px; + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; + cursor: pointer; +} + +/* Connection Modal - optgroup Styles */ +#modal-connection .form-select optgroup { + font-weight: bold; + color: var(--colortext); +} + +/* Connection Modal - form-select/form-input global */ +#modal-connection .form-select, +#modal-connection .form-input { + width: 100%; + padding: 12px; + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); + font-size: 16px; + box-sizing: border-box; +} + +#modal-connection .form-select { + -webkit-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 24px; } /* ============================================ diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 6c795cb..4f3e605 100755 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -7406,27 +7406,45 @@ // Check if carrier has position data (use typeof to allow 0 values) if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') { - // Find last equipment position - var lastPos = 0; + // Belegte Slots ermitteln (1-basiert) + var totalTE = parseInt(carrier.total_te) || 12; + var occupied = {}; var lastEquipment = null; + var lastEndPos = 0; carrierEquipment.forEach(function(eq) { - var endPos = (parseInt(eq.position_te) || 1) + (parseInt(eq.width_te) || 1) - 1; - if (endPos > lastPos) { - lastPos = endPos; + var pos = parseInt(eq.position_te) || 1; + var w = parseInt(eq.width_te) || 1; + var endPos = pos + w - 1; + for (var s = pos; s <= endPos; s++) { + occupied[s] = true; + } + if (endPos > lastEndPos) { + lastEndPos = endPos; lastEquipment = eq; } }); + // Maximale zusammenhängende Lücke berechnen + var maxGap = 0; + var currentGap = 0; + for (var s = 1; s <= totalTE; s++) { + if (!occupied[s]) { + currentGap++; + if (currentGap > maxGap) maxGap = currentGap; + } else { + currentGap = 0; + } + } + + // Carrier-Objekt merkt sich maximale Lücke für Typ-Filter + carrier._maxGap = maxGap; + // Calculate button positions - place buttons on the LEFT side of the carrier // Rail label is at about carrier._x - 44, so buttons need to be further left var btnX = carrier._x - 100; // Left of the carrier and rail label var btnY = carrier._y - self.BLOCK_HEIGHT / 2 + 10; // Aligned with blocks - // Check if there's space left - var totalTE = parseInt(carrier.total_te) || 12; - console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): lastPos=' + lastPos + ', totalTE=' + totalTE + ', hasSpace=' + (lastPos < totalTE) + ', lastEquipment=' + (lastEquipment ? lastEquipment.id : 'none')); - - if (lastPos < totalTE) { + if (maxGap > 0) { // Add Equipment button - positioned left of carrier var $addEquipment = $(''); $controls.append($addEquipment); @@ -7498,7 +7516,10 @@ $(document).off('click.addEquipment').on('click.addEquipment', '.schematic-add-equipment', function(e) { e.preventDefault(); var carrierId = $(this).data('carrier-id'); - self.showAddEquipmentDialog(carrierId); + // Maximale Lücke vom Carrier holen für Typ-Filter + var carrier = self.carriers.find(function(c) { return String(c.id) === String(carrierId); }); + var maxGap = carrier ? (carrier._maxGap || 99) : 99; + self.showAddEquipmentDialog(carrierId, maxGap); }); // Copy Equipment (single copy) @@ -8010,8 +8031,9 @@ }); }, - showAddEquipmentDialog: function(carrierId) { + showAddEquipmentDialog: function(carrierId, maxGap) { var self = this; + maxGap = maxGap || 99; // Load equipment types first $.ajax({ @@ -8020,13 +8042,13 @@ dataType: 'json', success: function(response) { if (response.success) { - self.showEquipmentTypeSelector(carrierId, response.types); + self.showEquipmentTypeSelector(carrierId, response.types, maxGap); } } }); }, - showEquipmentTypeSelector: function(carrierId, types) { + showEquipmentTypeSelector: function(carrierId, types, maxGap) { var self = this; // Kategorisiere Equipment-Typen @@ -8037,8 +8059,12 @@ 'klemme': { label: 'Klemmen', icon: 'fa-th-list', items: [] } }; - // Sortiere Typen in Kategorien + // Sortiere Typen in Kategorien (nur wenn Breite in verfügbare Lücke passt) + maxGap = maxGap || 99; types.forEach(function(t) { + var typeWidth = parseInt(t.width_te) || 1; + if (typeWidth > maxGap) return; // Passt nicht in verfügbare Lücke + var ref = (t.ref || '').toUpperCase(); var label = (t.label || '').toLowerCase(); diff --git a/js/pwa.js b/js/pwa.js index 3709260..8f5f8df 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -18,6 +18,7 @@ // Current selection customerId: null, customerName: '', + customerAddress: '', anlageId: null, anlageName: '', @@ -27,6 +28,7 @@ equipment: [], equipmentTypes: [], outputs: [], + inputs: [], // Offline queue offlineQueue: [], @@ -34,10 +36,16 @@ // Current modal state currentCarrierId: null, + editCarrierId: null, // null = Add-Modus, ID = Edit-Modus (Hutschiene) selectedTypeId: null, + editEquipmentId: null, // null = Add-Modus, ID = Edit-Modus + confirmCallback: null, // Callback für Bestätigungsdialog + editConnectionId: null, // null = Neu, ID = Edit + connectionEquipmentId: null, // Equipment für aktuelle Connection + connectionDirection: 'output', // 'output' oder 'input' + mediumTypes: null, // Kabeltypen aus DB (gecacht) + cachedTypeFields: null, // Equipment-Felder Cache - // Abgang-Labels: 'top' (Standard, wie echtes Panel) oder 'bottom' - labelsPosition: localStorage.getItem('kundenkarte_labels_pos') || 'top', }; // ============================================ @@ -77,6 +85,7 @@ if (lastState.customerId) { App.customerId = lastState.customerId; App.customerName = lastState.customerName || ''; + App.customerAddress = lastState.customerAddress || ''; $('#customer-name').text(App.customerName); } if (lastState.anlageId) { @@ -85,11 +94,15 @@ $('#anlage-name').text(App.anlageName); } - // Screen wiederherstellen + // Screen wiederherstellen inkl. vollständiger History-Stack + // Damit Zurück-Button auch nach App-Suspend korrekt funktioniert if (lastState.screen === 'editor' && App.anlageId) { + history.replaceState({ screen: 'search' }, '', '#search'); + history.pushState({ screen: 'anlagen' }, '', '#anlagen'); showScreen('editor'); loadEditorData(); } else if (lastState.screen === 'anlagen' && App.customerId) { + history.replaceState({ screen: 'search' }, '', '#search'); showScreen('anlagen'); reloadAnlagen(); } else { @@ -100,8 +113,10 @@ } } - // Initialen History-State setzen - history.replaceState({ screen: $('.screen.active').attr('id')?.replace('screen-', '') || 'login' }, ''); + // Initialen History-State setzen (nur wenn kein Session-Restore) + if (!sessionStorage.getItem('kundenkarte_pwa_state')) { + history.replaceState({ screen: $('.screen.active').attr('id')?.replace('screen-', '') || 'login' }, ''); + } // Load offline queue const storedQueue = localStorage.getItem('kundenkarte_offline_queue'); @@ -129,8 +144,22 @@ // Browser/Hardware Zurück-Button window.addEventListener('popstate', function(e) { + // Wenn ein Modal offen ist: Modal schließen statt navigieren + const $activeModal = $('.modal.active'); + if ($activeModal.length) { + $activeModal.removeClass('active'); + // Aktuellen State wieder pushen (Navigation verhindern) + const currentScreen = $('.screen.active').attr('id')?.replace('screen-', '') || 'search'; + history.pushState({ screen: currentScreen }, '', '#' + currentScreen); + return; + } + if (e.state && e.state.screen) { showScreen(e.state.screen, true); + // Anlagen-Liste nachladen falls leer (z.B. nach Seiten-Refresh) + if (e.state.screen === 'anlagen' && App.customerId && !$('#anlagen-list').children('.anlage-card, .contact-group').length) { + reloadAnlagen(); + } } else { // Kein State = zurück zum Anfang const activeScreen = App.token ? 'search' : 'login'; @@ -151,15 +180,38 @@ $('#btn-save-panel').on('click', handleSavePanel); $('#editor-content').on('click', '.btn-add-carrier', handleAddCarrier); + $('#editor-content').on('click', '.carrier-header', handleCarrierClick); $('#btn-save-carrier').on('click', handleSaveCarrier); + $('#btn-delete-carrier').on('click', handleDeleteCarrierConfirm); $('#editor-content').on('click', '.btn-add-equipment', handleAddEquipment); $('#editor-content').on('click', '.equipment-block', handleEquipmentClick); // Equipment modal $('#type-grid').on('click', '.type-btn', handleTypeSelect); + $('#btn-eq-back').on('click', () => showEquipmentStep('type')); $('#btn-save-equipment').on('click', handleSaveEquipment); $('#btn-cancel-equipment').on('click', () => closeModal('add-equipment')); + $('#btn-delete-equipment').on('click', handleDeleteEquipmentConfirm); + + // Terminal/Connection - Klick auf einzelne Klemme + $('#editor-content').on('click', '.terminal-point', handleTerminalClick); + $('#btn-save-connection').on('click', handleSaveConnection); + $('#btn-delete-connection').on('click', handleDeleteConnectionConfirm); + // Abgangsseite-Buttons + $('#conn-side-grid').on('click', '.side-btn', function() { + $('.side-btn').removeClass('selected'); + $(this).addClass('selected'); + }); + + // Bestätigungsdialog + $('#btn-confirm-ok').on('click', function() { + closeModal('confirm'); + if (App.confirmCallback) { + App.confirmCallback(); + App.confirmCallback = null; + } + }); // TE buttons $('.te-btn').on('click', function() { @@ -175,16 +227,6 @@ // Sync button $('#btn-sync').on('click', handleRefresh); - // Abgang-Labels Toggle (oben/unten) - $('#btn-toggle-labels').on('click', function() { - App.labelsPosition = App.labelsPosition === 'top' ? 'bottom' : 'top'; - localStorage.setItem('kundenkarte_labels_pos', App.labelsPosition); - $(this).removeClass('labels-top labels-bottom').addClass('labels-' + App.labelsPosition); - renderEditor(); - }); - - // Initialen Toggle-Zustand setzen - $('#btn-toggle-labels').removeClass('labels-top labels-bottom').addClass('labels-' + App.labelsPosition); } // ============================================ @@ -260,6 +302,7 @@ screen: screen || 'search', customerId: App.customerId, customerName: App.customerName, + customerAddress: App.customerAddress, anlageId: App.anlageId, anlageName: App.anlageName }; @@ -361,9 +404,11 @@ async function handleCustomerSelect() { const id = $(this).data('id'); const name = $(this).find('.list-item-title').text(); + const address = $(this).find('.list-item-subtitle').text(); App.customerId = id; App.customerName = name; + App.customerAddress = address; $('#customer-name').text(name); showScreen('anlagen'); @@ -401,31 +446,37 @@ function renderAnlagenList(anlagen, contacts) { let html = ''; - // Kunden-Anlagen (ohne Kontaktzuweisung) - if (anlagen && anlagen.length) { - anlagen.forEach(a => { - html += renderAnlageCard(a); - }); - } - - // Kontakt-Adressen als Gruppen + // Kontakt-Adressen (Gebäude/Standorte) als Liste if (contacts && contacts.length) { + html += '
Kennlinie + Ampere:
'; - html += 'Ampere:
'; - html += 'Empfindlichkeit:
'; - html += 'Ampere:
'; - html += 'Kennlinie + Ampere:
'; - html += '