diff --git a/CLAUDE.md b/CLAUDE.md index 2daee7d..31d6e58 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,27 +130,58 @@ Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte. Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentation vor Ort. ### Dateien -- `pwa.php` - Haupteinstieg (HTML/CSS/JS Container) +- `pwa.php` - Haupteinstieg (HTML/CSS/JS Container, lädt jQuery aus Dolibarr) - `pwa_auth.php` - Token-basierte Authentifizierung (15 Tage gültig) - `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA -- `js/pwa.js` - Komplette App-Logik (vanilla JS, kein jQuery) -- `css/pwa.css` - Mobile-First Dark Mode Design -- `sw.js` - Service Worker für Offline-Cache +- `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) - `manifest.json` - Web App Manifest für Installation ### Workflow 1. Login mit Dolibarr-Credentials → Token wird lokal gespeichert 2. Kunde suchen → Anlagen werden gecached -3. Anlage mit Schaltplan-Editor auswählen → Daten werden gecached -4. Offline arbeiten: Felder, Hutschienen, Automaten hinzufügen -5. Änderungen werden in lokaler Queue gespeichert -6. Bei Internetverbindung: Automatische Synchronisierung +3. Kontakt-Adressen (Gebäude/Standorte) aufklappen → Anlagen pro Adresse +4. Anlage mit Schaltplan-Editor auswählen → Daten werden gecached +5. Offline arbeiten: Hutschienen, Automaten hinzufügen +6. Änderungen werden in lokaler Queue gespeichert +7. Bei Internetverbindung: Automatische Synchronisierung + +### Design-System +- CSS-Variablen basierend auf Dolibarr Dark Theme (`--colorbackbody`, `--colortext`, etc.) +- Theme-Farbe `--primary` wird dynamisch aus Dolibarr-Config geladen (`THEME_ELDY_TOPMENU_BACK1`) +- Buttons: `--butactionbg` (goldbraun) statt blau +- Header: sticky, primary-Farbe + +### Kontakt-Adressen +- `get_anlagen` API liefert jetzt `anlagen` (Kunden-Ebene) + `contacts` (Adressen) +- Kontakt-Gruppen werden als aufklappbare Akkordeons dargestellt +- `get_contact_anlagen` API lädt Anlagen pro Kontakt-Adresse bei Bedarf +- Kontakt-Adressen zeigen Name, Adresse, Ort und Anzahl Anlagen + +### 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` +- `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 + +### 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 +- Sync-Button lädt jetzt erst Offline-Queue, dann Daten neu ### Token-Authentifizierung - Tokens enthalten: user_id, login, created, expires, hash - Hash = MD5(user_id + login + MAIN_SECURITY_SALT) - Gültigkeit: 15 Tage - Gespeichert in localStorage +- `$user->getrights()` wird nach Token-Validierung aufgerufen ### Offline-Sync - Alle Änderungen werden in `offlineQueue` (localStorage) gespeichert diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index 9fadc93..ee91201 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -58,6 +58,7 @@ if ($tokenData['hash'] !== $expectedHash) { require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; $user = new User($db); $user->fetch($tokenData['user_id']); +$user->getrights(); if ($user->id <= 0 || $user->statut != 1) { echo json_encode(array('success' => false, 'error' => 'Benutzer nicht mehr aktiv')); @@ -76,6 +77,7 @@ require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.p require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php'; 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'; $action = GETPOST('action', 'aZ09'); @@ -117,7 +119,7 @@ switch ($action) { break; // ============================================ - // GET ANLAGEN FOR CUSTOMER + // GET ANLAGEN FOR CUSTOMER (inkl. Kontakt-Adressen) // ============================================ case 'get_anlagen': $customerId = GETPOSTINT('customer_id'); @@ -126,20 +128,72 @@ switch ($action) { break; } + // Root-Anlagen ohne Kontaktzuweisung (Kunden-Ebene) $anlage = new Anlage($db); - $anlagen = $anlage->fetchAll('ASC', 'label', 0, 0, array('fk_soc' => $customerId)); + $anlagen = $anlage->fetchChildren(0, $customerId); $result = array(); - if (is_array($anlagen)) { - foreach ($anlagen as $a) { - $result[] = array( - 'id' => $a->id, - 'label' => $a->label, - 'has_editor' => !empty($a->schematic_editor_enabled) + foreach ($anlagen as $a) { + $result[] = array( + 'id' => $a->id, + 'label' => $a->label, + 'type' => $a->type_label, + 'has_editor' => !empty($a->schematic_editor_enabled) + ); + } + + // Kontakt-Adressen mit Anlagen laden + $contacts = array(); + $sql = "SELECT c.rowid, c.lastname, c.firstname, c.address, c.town,"; + $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage a WHERE a.fk_contact = c.rowid AND a.status = 1) as anlage_count"; + $sql .= " FROM ".MAIN_DB_PREFIX."socpeople as c"; + $sql .= " WHERE c.fk_soc = ".((int) $customerId); + $sql .= " AND c.statut = 1"; + $sql .= " ORDER BY c.lastname ASC"; + + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $contactName = trim($obj->lastname.' '.$obj->firstname); + $contacts[] = array( + 'id' => $obj->rowid, + 'name' => $contactName, + 'address' => $obj->address, + 'town' => $obj->town, + 'anlage_count' => (int) $obj->anlage_count ); } } + $response['success'] = true; + $response['anlagen'] = $result; + $response['contacts'] = $contacts; + break; + + // ============================================ + // GET ANLAGEN FOR CONTACT ADDRESS + // ============================================ + case 'get_contact_anlagen': + $customerId = GETPOSTINT('customer_id'); + $contactId = GETPOSTINT('contact_id'); + if ($customerId <= 0 || $contactId <= 0) { + $response['error'] = 'Kunden-ID und Kontakt-ID erforderlich'; + break; + } + + $anlage = new Anlage($db); + $anlagen = $anlage->fetchChildrenByContact(0, $customerId, $contactId); + + $result = array(); + foreach ($anlagen as $a) { + $result[] = array( + 'id' => $a->id, + 'label' => $a->label, + 'type' => $a->type_label, + 'has_editor' => !empty($a->schematic_editor_enabled) + ); + } + $response['success'] = true; $response['anlagen'] = $result; break; @@ -195,11 +249,37 @@ switch ($action) { 'label' => $eq->label, 'position_te' => $eq->position_te, 'width_te' => $eq->width_te, + 'block_label' => $eq->getBlockLabel(), + 'block_color' => $eq->getBlockColor(), 'field_values' => $eq->getFieldValues() ); } } + // 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 .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection"; + $sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")"; + $sql .= " AND fk_target IS NULL"; + $sql .= " AND status = 1"; + $resql = $db->query($sql); + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $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 + ); + } + } + } + // Load equipment types $eqType = new EquipmentType($db); $types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive @@ -219,6 +299,7 @@ switch ($action) { $response['panels'] = $panelsData; $response['carriers'] = $carriersData; $response['equipment'] = $equipmentData; + $response['outputs'] = $outputsData; $response['types'] = $typesData; break; diff --git a/css/pwa.css b/css/pwa.css index 0b917e4..d809db9 100644 --- a/css/pwa.css +++ b/css/pwa.css @@ -1,30 +1,25 @@ /** * KundenKarte PWA Styles - * Mobile-First, Touch-optimiert, Dark Mode + * Design-System basierend auf Dolibarr Dark Theme + * Mobile-First, Touch-optimiert */ :root { - --primary: #3498db; - --primary-dark: #2980b9; - --success: #27ae60; - --warning: #f39c12; - --danger: #e74c3c; - - --bg-body: #1a1a2e; - --bg-card: #16213e; - --bg-input: #0f3460; - --bg-header: #0f3460; - - --text: #eee; - --text-muted: #888; - --text-dim: #666; - - --border: #2a2a4a; - --border-light: #3a3a5a; - - --shadow: 0 4px 20px rgba(0,0,0,0.4); - --radius: 12px; - --radius-sm: 8px; + --primary: #0077b3; + --colorbackbody: #1d1e20; + --colorbackcard: #1d1e20; + --colorbacktitle: #3b3c3e; + --colorbackline: #38393d; + --colorbackinput: rgb(70, 70, 70); + --colortext: rgb(220,220,220); + --colortextmuted: rgb(180,180,180); + --colortextlink: #4390dc; + --colorborder: #2b2c2e; + --butactionbg: rgb(173,140,79); + --textbutaction: rgb(255,255,255); + --success: #25a580; + --danger: #993013; + --warning: #bc9526; /* Safe areas für notches */ --safe-top: env(safe-area-inset-top, 0px); @@ -45,8 +40,8 @@ html, body { body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; - background: var(--bg-body); - color: var(--text); + background: var(--colorbackbody); + color: var(--colortext); font-size: 16px; line-height: 1.4; } @@ -86,12 +81,15 @@ body { display: flex; align-items: center; gap: 12px; - padding: 12px 16px; - padding-top: calc(12px + var(--safe-top)); - background: var(--bg-header); - border-bottom: 1px solid var(--border); - min-height: 60px; + padding: 14px 16px; + padding-top: calc(14px + var(--safe-top)); + background: var(--primary); + color: #fff; + min-height: 56px; flex-shrink: 0; + position: sticky; + top: 0; + z-index: 100; } .header h1 { @@ -113,17 +111,17 @@ body { display: flex; align-items: center; justify-content: center; - background: transparent; + background: rgba(255,255,255,0.15); border: none; - border-radius: 50%; - color: var(--text); + border-radius: 8px; + color: #fff; cursor: pointer; transition: background 0.2s; position: relative; } .btn-icon:active { - background: rgba(255,255,255,0.1); + background: rgba(255,255,255,0.3); } .btn-icon svg { @@ -138,8 +136,8 @@ body { .sync-badge { position: absolute; - top: 4px; - right: 4px; + top: 2px; + right: 2px; background: var(--danger); color: #fff; font-size: 11px; @@ -171,37 +169,39 @@ body { } .login-logo { - width: 80px; - height: 80px; - background: var(--primary); - border-radius: 20px; + width: 72px; + height: 72px; display: flex; align-items: center; justify-content: center; - margin-bottom: 24px; + margin-bottom: 20px; } .login-logo svg { - width: 48px; - height: 48px; - fill: #fff; + width: 64px; + height: 64px; + fill: var(--primary); } .login-title { - font-size: 28px; - font-weight: 700; + font-size: 24px; + font-weight: 600; margin-bottom: 8px; } .login-subtitle { font-size: 14px; - color: var(--text-muted); - margin-bottom: 32px; + color: var(--colortextmuted); + margin-bottom: 28px; } .login-form { width: 100%; - max-width: 320px; + max-width: 340px; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 12px; + padding: 28px 24px; } /* ============================================ @@ -214,8 +214,8 @@ body { .form-group label { display: block; - font-size: 13px; - color: var(--text-muted); + font-size: 14px; + color: var(--colortextmuted); margin-bottom: 6px; } @@ -224,10 +224,10 @@ body { width: 100%; padding: 14px 16px; font-size: 16px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); outline: none; transition: border-color 0.2s; } @@ -238,15 +238,23 @@ body { } .form-group input::placeholder { - color: var(--text-dim); + color: var(--colortextmuted); + opacity: 0.6; } .error-text { - color: var(--danger); + color: #fff; + background: var(--danger); + padding: 10px 14px; + border-radius: 8px; font-size: 13px; margin-top: 12px; text-align: center; - min-height: 20px; + min-height: 0; +} + +.error-text:empty { + display: none; } /* ============================================ @@ -262,7 +270,7 @@ body { font-size: 16px; font-weight: 600; border: none; - border-radius: var(--radius-sm); + border-radius: 8px; cursor: pointer; transition: all 0.2s; white-space: nowrap; @@ -273,17 +281,18 @@ body { } .btn-primary { - background: var(--primary); - color: #fff; + background: var(--butactionbg); + color: var(--textbutaction); } -.btn-primary:active { - background: var(--primary-dark); +.btn-primary:hover { + opacity: 0.9; } .btn-secondary { - background: var(--border); - color: var(--text); + background: var(--colorbacktitle); + color: var(--colortext); + border: 1px solid var(--colorborder); } .btn-success { @@ -300,6 +309,13 @@ body { width: 100%; padding: 16px; font-size: 17px; + font-weight: 600; + background: var(--butactionbg); + color: var(--textbutaction); + border: none; + border-radius: 8px; + cursor: pointer; + margin-top: 8px; } /* ============================================ @@ -309,6 +325,8 @@ body { .search-container { padding: 16px; flex-shrink: 0; + background: var(--colorbacktitle); + border-bottom: 1px solid var(--colorborder); } .search-box { @@ -316,15 +334,15 @@ body { align-items: center; gap: 12px; padding: 12px 16px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: var(--radius); + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 8px; } .search-box svg { width: 22px; height: 22px; - fill: var(--text-muted); + fill: var(--colortextmuted); flex-shrink: 0; } @@ -332,13 +350,14 @@ body { flex: 1; background: transparent; border: none; - color: var(--text); + color: var(--colortext); font-size: 16px; outline: none; } .search-box input::placeholder { - color: var(--text-dim); + color: var(--colortextmuted); + opacity: 0.6; } /* ============================================ @@ -349,24 +368,24 @@ body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; - padding: 0 16px 16px; + padding: 12px 16px 16px; } .list-item { display: flex; align-items: center; gap: 14px; - padding: 16px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - margin-bottom: 10px; + padding: 14px 16px; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 8px; + margin-bottom: 8px; cursor: pointer; transition: all 0.2s; } .list-item:active { - background: var(--bg-input); + background: var(--colorbackinput); transform: scale(0.99); } @@ -402,20 +421,20 @@ body { .list-item-subtitle { font-size: 13px; - color: var(--text-muted); + color: var(--colortextmuted); margin-top: 2px; } .list-item-arrow { width: 20px; height: 20px; - fill: var(--text-dim); + fill: var(--colortextmuted); } .list-empty { text-align: center; padding: 48px 24px; - color: var(--text-muted); + color: var(--colortextmuted); } /* ============================================ @@ -423,43 +442,52 @@ body { ============================================ */ .anlagen-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; + display: flex; + flex-wrap: wrap; + gap: 10px; padding: 16px; } +.anlagen-grid > .anlage-card { + width: calc(50% - 5px); + box-sizing: border-box; +} + +.anlagen-grid > .contact-group { + width: 100%; +} + .anlage-card { display: flex; flex-direction: column; align-items: center; - padding: 20px 16px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); + padding: 18px 14px; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 8px; cursor: pointer; transition: all 0.2s; } .anlage-card:active { - background: var(--bg-input); + background: var(--colorbackinput); transform: scale(0.98); } .anlage-card-icon { - width: 56px; - height: 56px; + width: 52px; + height: 52px; background: var(--success); - border-radius: 14px; + border-radius: 12px; display: flex; align-items: center; justify-content: center; - margin-bottom: 12px; + margin-bottom: 10px; } .anlage-card-icon svg { - width: 32px; - height: 32px; + width: 28px; + height: 28px; fill: #fff; } @@ -470,6 +498,117 @@ body { word-break: break-word; } +.anlage-card-type { + font-size: 11px; + color: var(--colortextmuted); + margin-top: 4px; +} + +/* ============================================ + CONTACT GROUPS + ============================================ */ + +.contact-group { + margin-bottom: 8px; +} + +.contact-group-header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; +} + +.contact-group-header:active { + background: var(--colorbackinput); +} + +.contact-group.expanded .contact-group-header { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-color: transparent; +} + +.contact-group-header svg { + width: 24px; + height: 24px; + fill: var(--warning); + flex-shrink: 0; +} + +.contact-group-name { + font-size: 14px; + font-weight: 600; +} + +.contact-group-address { + font-size: 12px; + color: var(--colortextmuted); + margin-top: 2px; +} + +.contact-group-count { + margin-left: auto; + background: var(--colorbackinput); + color: var(--colortextmuted); + font-size: 12px; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + min-width: 24px; + text-align: center; +} + +.contact-anlagen-list { + padding: 10px; + 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; + 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, +.contact-anlagen-list .list-empty { + width: 100%; +} + +.list-empty.small { + font-size: 13px; + padding: 12px; +} + +.spinner.small { + width: 24px; + height: 24px; + border-width: 3px; +} + /* ============================================ EDITOR ============================================ */ @@ -478,15 +617,15 @@ body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; - padding: 16px; + padding: 12px; padding-bottom: 100px; } .panel-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - margin-bottom: 16px; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 8px; + margin-bottom: 12px; overflow: hidden; } @@ -494,13 +633,13 @@ body { display: flex; align-items: center; justify-content: space-between; - padding: 14px 16px; - background: var(--bg-header); - border-bottom: 1px solid var(--border); + padding: 12px 14px; + background: var(--colorbacktitle); + border-bottom: 1px solid var(--colorborder); } .panel-title { - font-size: 16px; + font-size: 15px; font-weight: 600; } @@ -510,15 +649,15 @@ body { } .panel-body { - padding: 12px; + padding: 10px; } /* Hutschiene */ .carrier-item { - background: var(--bg-input); - border: 1px solid var(--border-light); - border-radius: var(--radius-sm); - margin-bottom: 10px; + background: var(--colorbackinput); + border: 1px solid var(--colorborder); + border-radius: 6px; + margin-bottom: 8px; overflow: hidden; } @@ -526,42 +665,51 @@ body { display: flex; align-items: center; justify-content: space-between; - padding: 10px 12px; + padding: 8px 10px; background: rgba(255,255,255,0.03); } .carrier-label { font-size: 13px; font-weight: 600; - color: var(--text-muted); + color: var(--colortextmuted); } .carrier-te { font-size: 12px; - color: var(--text-dim); + color: var(--colortextmuted); + opacity: 0.7; } .carrier-body { - padding: 10px; + padding: 5px; display: flex; - flex-wrap: wrap; - gap: 6px; + align-items: stretch; + gap: 3px; min-height: 50px; } +.carrier-grid { + flex: 1; + min-width: 0; + display: grid; + gap: 2px; +} + /* Equipment Block */ .equipment-block { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-width: 44px; - height: 60px; - padding: 4px 8px; + min-width: 0; + height: 52px; + padding: 2px 1px; background: var(--primary); - border-radius: 6px; + border-radius: 4px; cursor: pointer; - transition: all 0.2s; + transition: all 0.15s ease; + overflow: hidden; } .equipment-block:active { @@ -569,24 +717,145 @@ body { } .equipment-block-type { - font-size: 11px; + font-size: 9px; font-weight: bold; color: #fff; + line-height: 1.1; } .equipment-block-value { - font-size: 13px; + font-size: 11px; font-weight: bold; color: #fff; + line-height: 1.1; } .equipment-block-label { - font-size: 9px; + font-size: 8px; color: rgba(255,255,255,0.7); - max-width: 50px; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + line-height: 1.1; +} + +/* Equipment Block Text (einzelner Block-Label wie "B16") */ +.equipment-block-text { + font-size: 11px; + font-weight: bold; + color: #fff; + line-height: 1.1; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + 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; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255,255,255,0.15); + border: none; + border-radius: 8px; + color: #fff; + cursor: pointer; + transition: background 0.2s, transform 0.3s; +} + +.btn-toggle-labels:active { + background: rgba(255,255,255,0.3); +} + +.btn-toggle-labels svg { + width: 22px; + height: 22px; + fill: currentColor; +} + +/* Pfeil nach oben = Labels oben */ +.btn-toggle-labels.labels-top svg { + transform: rotate(0deg); +} + +/* Pfeil nach unten = Labels unten */ +.btn-toggle-labels.labels-bottom svg { + transform: rotate(180deg); } /* Add Button in Carrier */ @@ -594,23 +863,31 @@ body { display: flex; align-items: center; justify-content: center; - min-width: 44px; - height: 60px; - padding: 8px; + width: 36px; + min-width: 36px; + flex-shrink: 0; + align-self: stretch; + padding: 4px; background: transparent; - border: 2px dashed var(--border-light); - border-radius: 6px; - color: var(--text-dim); + border: 2px dashed var(--colorborder); + border-radius: 4px; + color: var(--colortextmuted); cursor: pointer; transition: all 0.2s; } -.btn-add-equipment:active { +.btn-add-equipment:active:not(.disabled) { background: rgba(255,255,255,0.05); border-color: var(--primary); color: var(--primary); } +.btn-add-equipment.disabled { + opacity: 0.25; + cursor: not-allowed; + border-style: dotted; +} + .btn-add-equipment svg { width: 24px; height: 24px; @@ -623,11 +900,11 @@ body { align-items: center; justify-content: center; gap: 8px; - padding: 14px; + padding: 12px; background: transparent; - border: 2px dashed var(--border); - border-radius: var(--radius-sm); - color: var(--text-muted); + border: 2px dashed var(--colorborder); + border-radius: 6px; + color: var(--colortextmuted); font-size: 14px; cursor: pointer; transition: all 0.2s; @@ -661,20 +938,20 @@ body { align-items: center; gap: 8px; padding: 14px 20px; - background: var(--primary); - color: #fff; + background: var(--butactionbg); + color: var(--textbutaction); border: none; border-radius: 30px; font-size: 15px; font-weight: 600; - box-shadow: var(--shadow); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); cursor: pointer; transition: all 0.2s; } .fab:active { transform: scale(0.95); - background: var(--primary-dark); + opacity: 0.9; } .fab svg { @@ -709,8 +986,9 @@ body { width: 100%; max-width: 500px; max-height: 85vh; - background: var(--bg-card); - border-radius: var(--radius) var(--radius) 0 0; + background: var(--colorbackline); + border: 1px solid var(--colorborder); + border-radius: 12px 12px 0 0; display: flex; flex-direction: column; overflow: hidden; @@ -735,7 +1013,8 @@ body { align-items: center; justify-content: space-between; padding: 16px 20px; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--colorborder); + background: var(--colorbacktitle); } .modal-header h2 { @@ -751,7 +1030,7 @@ body { justify-content: center; background: transparent; border: none; - color: var(--text-muted); + color: var(--colortextmuted); font-size: 28px; cursor: pointer; border-radius: 50%; @@ -771,7 +1050,7 @@ body { display: flex; gap: 12px; padding: 16px 20px; - border-top: 1px solid var(--border); + border-top: 1px solid var(--colorborder); } .modal-footer .btn { @@ -782,24 +1061,23 @@ body { TYPE GRID ============================================ */ -.step { +.step-values-area { display: none; -} - -.step.active { - display: block; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--colorborder); } .step-label { font-size: 14px; - color: var(--text-muted); + color: var(--colortextmuted); margin-bottom: 12px; } .type-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 10px; + gap: 8px; } .type-btn { @@ -807,19 +1085,19 @@ body { flex-direction: column; align-items: center; justify-content: center; - padding: 16px 8px; - background: var(--bg-input); - border: 2px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); + padding: 14px 6px; + background: var(--colorbackinput); + border: 2px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); cursor: pointer; transition: all 0.2s; } .type-btn:active, .type-btn.selected { - border-color: var(--primary); - background: rgba(52, 152, 219, 0.2); + border-color: var(--butactionbg); + background: rgba(173, 140, 79, 0.15); } .type-btn-icon { @@ -841,10 +1119,10 @@ body { .te-btn { padding: 20px; - background: var(--bg-input); - border: 2px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); + background: var(--colorbackinput); + border: 2px solid var(--colorborder); + border-radius: 8px; + color: var(--colortext); font-size: 18px; font-weight: 600; cursor: pointer; @@ -853,8 +1131,8 @@ body { .te-btn:active, .te-btn.selected { - border-color: var(--primary); - background: rgba(52, 152, 219, 0.2); + border-color: var(--butactionbg); + background: rgba(173, 140, 79, 0.15); } /* Value Quick Select */ @@ -867,10 +1145,10 @@ body { .value-chip { padding: 10px 16px; - background: var(--bg-input); - border: 1px solid var(--border); + background: var(--colorbackinput); + border: 1px solid var(--colorborder); border-radius: 20px; - color: var(--text); + color: var(--colortext); font-size: 14px; font-weight: 500; cursor: pointer; @@ -879,8 +1157,9 @@ body { .value-chip:active, .value-chip.selected { - background: var(--primary); - border-color: var(--primary); + background: var(--butactionbg); + border-color: var(--butactionbg); + color: var(--textbutaction); } /* ============================================ @@ -914,16 +1193,17 @@ body { position: fixed; bottom: 80px; left: 50%; - transform: translateX(-50%) translateY(100px); - padding: 14px 24px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text); - font-size: 14px; - box-shadow: var(--shadow); + transform: translateX(-50%) translateY(15px); + padding: 14px 28px; + background: var(--colorbacktitle); + border: 1px solid var(--colorborder); + border-radius: 10px; + color: var(--colortext); + font-size: 15px; + font-weight: 500; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); opacity: 0; - transition: all 0.3s; + transition: all 0.3s ease; z-index: 2000; max-width: 90%; text-align: center; @@ -935,11 +1215,21 @@ body { } .toast.success { + background: var(--success); border-color: var(--success); + color: #fff; } .toast.error { + background: var(--danger); border-color: var(--danger); + color: #fff; +} + +.toast.warning { + background: var(--warning); + border-color: var(--warning); + color: #000; } /* ============================================ @@ -955,14 +1245,14 @@ body { } .text-muted { - color: var(--text-muted); + color: var(--colortextmuted); } /* Loading Spinner */ .spinner { width: 40px; height: 40px; - border: 3px solid var(--border); + border: 3px solid var(--colorborder); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; @@ -980,3 +1270,39 @@ body { justify-content: center; gap: 16px; } + +/* ============================================ + MOBILE OPTIMIERUNGEN + ============================================ */ + +@media (max-width: 768px) { + .type-grid { + grid-template-columns: repeat(3, 1fr); + gap: 6px; + } + + .type-btn { + padding: 12px 4px; + } + + .fab { + padding: 12px 18px; + font-size: 14px; + } +} + +/* Touch Targets */ +@media (pointer: coarse) { + .btn, + .btn-large, + .te-btn, + .list-item, + .contact-group-header, + .anlage-card { + min-height: 48px; + } + + .value-chip { + min-height: 44px; + } +} diff --git a/js/pwa.js b/js/pwa.js index edc2917..3709260 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -3,7 +3,7 @@ * Offline-First App für Elektriker */ -(function() { +(function($) { 'use strict'; // ============================================ @@ -26,6 +26,7 @@ carriers: [], equipment: [], equipmentTypes: [], + outputs: [], // Offline queue offlineQueue: [], @@ -34,6 +35,9 @@ // Current modal state currentCarrierId: null, selectedTypeId: null, + + // Abgang-Labels: 'top' (Standard, wie echtes Panel) oder 'bottom' + labelsPosition: localStorage.getItem('kundenkarte_labels_pos') || 'top', }; // ============================================ @@ -66,9 +70,39 @@ if (storedToken && storedUser) { App.token = storedToken; App.user = JSON.parse(storedUser); - showScreen('search'); + + // Letzten Zustand wiederherstellen + const lastState = JSON.parse(sessionStorage.getItem('kundenkarte_pwa_state') || 'null'); + if (lastState && lastState.screen) { + if (lastState.customerId) { + App.customerId = lastState.customerId; + App.customerName = lastState.customerName || ''; + $('#customer-name').text(App.customerName); + } + if (lastState.anlageId) { + App.anlageId = lastState.anlageId; + App.anlageName = lastState.anlageName || ''; + $('#anlage-name').text(App.anlageName); + } + + // Screen wiederherstellen + if (lastState.screen === 'editor' && App.anlageId) { + showScreen('editor'); + loadEditorData(); + } else if (lastState.screen === 'anlagen' && App.customerId) { + showScreen('anlagen'); + reloadAnlagen(); + } else { + showScreen('search'); + } + } else { + showScreen('search'); + } } + // Initialen History-State setzen + history.replaceState({ screen: $('.screen.active').attr('id')?.replace('screen-', '') || 'login' }, ''); + // Load offline queue const storedQueue = localStorage.getItem('kundenkarte_offline_queue'); if (storedQueue) { @@ -90,8 +124,19 @@ $('#btn-logout').on('click', handleLogout); // Navigation - $('#btn-back-search').on('click', () => showScreen('search')); - $('#btn-back-anlagen').on('click', () => showScreen('anlagen')); + $('#btn-back-search').on('click', () => history.back()); + $('#btn-back-anlagen').on('click', () => history.back()); + + // Browser/Hardware Zurück-Button + window.addEventListener('popstate', function(e) { + if (e.state && e.state.screen) { + showScreen(e.state.screen, true); + } else { + // Kein State = zurück zum Anfang + const activeScreen = App.token ? 'search' : 'login'; + showScreen(activeScreen, true); + } + }); // Search $('#search-customer').on('input', debounce(handleSearch, 300)); @@ -99,6 +144,7 @@ // Customer/Anlage selection $('#customer-list').on('click', '.list-item', handleCustomerSelect); $('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect); + $('#anlagen-list').on('click', '.contact-group-header', handleContactGroupClick); // Editor actions $('#btn-add-panel').on('click', () => openModal('add-panel')); @@ -127,7 +173,18 @@ }); // Sync button - $('#btn-sync').on('click', syncOfflineChanges); + $('#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); } // ============================================ @@ -170,8 +227,13 @@ function handleLogout() { App.token = null; App.user = null; + App.customerId = null; + App.customerName = ''; + App.anlageId = null; + App.anlageName = ''; localStorage.removeItem('kundenkarte_pwa_token'); localStorage.removeItem('kundenkarte_pwa_user'); + sessionStorage.removeItem('kundenkarte_pwa_state'); showScreen('login'); } @@ -179,14 +241,62 @@ // SCREENS // ============================================ - function showScreen(name) { + function showScreen(name, skipHistory) { $('.screen').removeClass('active'); $('#screen-' + name).addClass('active'); - // Load data if needed - if (name === 'search') { - $('#search-customer').val('').focus(); - $('#customer-list').html('
Suchbegriff eingeben...
'); + // Browser-History für Zurück-Button + if (!skipHistory) { + history.pushState({ screen: name }, '', '#' + name); + } + + // State speichern für Refresh-Wiederherstellung + saveState(name); + } + + // Zustand in sessionStorage speichern + function saveState(screen) { + const state = { + screen: screen || 'search', + customerId: App.customerId, + customerName: App.customerName, + anlageId: App.anlageId, + anlageName: App.anlageName + }; + sessionStorage.setItem('kundenkarte_pwa_state', JSON.stringify(state)); + } + + // Anlagen-Liste für aktuellen Kunden neu laden + async function reloadAnlagen() { + if (!App.customerId) return; + + $('#anlagen-list').html('
'); + + try { + const response = await apiCall('ajax/pwa_api.php', { + action: 'get_anlagen', + customer_id: App.customerId + }); + + if (response.success) { + renderAnlagenList(response.anlagen, response.contacts || []); + localStorage.setItem('kundenkarte_anlagen_' + App.customerId, JSON.stringify({ + anlagen: response.anlagen, + contacts: response.contacts || [] + })); + } else { + $('#anlagen-list').html('
Keine Anlagen gefunden
'); + } + } catch (err) { + // Gecachte Daten verwenden + const cached = localStorage.getItem('kundenkarte_anlagen_' + App.customerId); + if (cached) { + const data = JSON.parse(cached); + renderAnlagenList(data.anlagen || data, data.contacts || []); + showToast('Offline - Zeige gecachte Daten', 'warning'); + } else { + $('#anlagen-list').html('
Fehler beim Laden
'); + } } } @@ -265,10 +375,13 @@ customer_id: id }); - if (response.success && response.anlagen) { - renderAnlagenList(response.anlagen); - // Cache for offline - localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify(response.anlagen)); + if (response.success) { + renderAnlagenList(response.anlagen, response.contacts || []); + // Cache für Offline + localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify({ + anlagen: response.anlagen, + contacts: response.contacts || [] + })); } else { $('#anlagen-list').html('
Keine Anlagen gefunden
'); } @@ -276,7 +389,8 @@ // Try cached const cached = localStorage.getItem('kundenkarte_anlagen_' + id); if (cached) { - renderAnlagenList(JSON.parse(cached)); + const data = JSON.parse(cached); + renderAnlagenList(data.anlagen || data, data.contacts || []); showToast('Offline - Zeige gecachte Daten', 'warning'); } else { $('#anlagen-list').html('
Fehler beim Laden
'); @@ -284,30 +398,56 @@ } } - function renderAnlagenList(anlagen) { - // Filter nur Anlagen mit Editor - const withEditor = anlagen.filter(a => a.has_editor); + function renderAnlagenList(anlagen, contacts) { + let html = ''; - if (!withEditor.length) { - $('#anlagen-list').html('
Keine Anlagen mit Schaltplan-Editor
'); + // Kunden-Anlagen (ohne Kontaktzuweisung) + if (anlagen && anlagen.length) { + anlagen.forEach(a => { + html += renderAnlageCard(a); + }); + } + + // Kontakt-Adressen als Gruppen + if (contacts && contacts.length) { + contacts.forEach(c => { + const subtitle = [c.address, c.town].filter(Boolean).join(', '); + html += ` +
+
+ +
+
${escapeHtml(c.name)}
+ ${subtitle ? '
' + escapeHtml(subtitle) + '
' : ''} +
+ ${c.anlage_count} +
+
+
+ `; + }); + } + + if (!html) { + $('#anlagen-list').html('
Keine Anlagen gefunden
'); return; } - let html = ''; - withEditor.forEach(a => { - html += ` -
-
- -
-
${escapeHtml(a.label || 'Anlage ' + a.id)}
-
- `; - }); - $('#anlagen-list').html(html); } + function renderAnlageCard(a) { + return ` +
+
+ +
+
${escapeHtml(a.label || 'Anlage ' + a.id)}
+ ${a.type ? '
' + escapeHtml(a.type) + '
' : ''} +
+ `; + } + async function handleAnlageSelect() { const id = $(this).data('id'); const name = $(this).find('.anlage-card-title').text(); @@ -320,6 +460,46 @@ await loadEditorData(); } + // ============================================ + // CONTACT GROUP EXPAND/COLLAPSE + // ============================================ + + async function handleContactGroupClick() { + const $group = $(this).closest('.contact-group'); + const $list = $group.find('.contact-anlagen-list'); + const contactId = $group.data('contact-id'); + const customerId = $group.data('customer-id'); + + // Toggle anzeigen/verstecken + if ($group.hasClass('expanded')) { + $group.removeClass('expanded'); + return; + } + + $group.addClass('expanded'); + $list.html('
'); + + try { + const response = await apiCall('ajax/pwa_api.php', { + action: 'get_contact_anlagen', + customer_id: customerId, + contact_id: contactId + }); + + if (response.success && response.anlagen && response.anlagen.length) { + let html = ''; + response.anlagen.forEach(a => { + html += renderAnlageCard(a); + }); + $list.html(html); + } else { + $list.html('
Keine Anlagen
'); + } + } catch (err) { + $list.html('
Fehler beim Laden
'); + } + } + // ============================================ // EDITOR // ============================================ @@ -338,13 +518,15 @@ App.carriers = response.carriers || []; App.equipment = response.equipment || []; App.equipmentTypes = response.types || []; + App.outputs = response.outputs || []; // Cache for offline localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({ panels: App.panels, carriers: App.carriers, equipment: App.equipment, - types: App.equipmentTypes + types: App.equipmentTypes, + outputs: App.outputs })); renderEditor(); @@ -358,6 +540,7 @@ App.carriers = data.carriers || []; App.equipment = data.equipment || []; App.equipmentTypes = data.types || []; + App.outputs = data.outputs || []; renderEditor(); showToast('Offline - Zeige gecachte Daten', 'warning'); } else { @@ -389,37 +572,94 @@ const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrier.id); carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0)); + const totalTe = parseInt(carrier.total_te) || 12; + const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseInt(eq.width_te) || 1), 0); + const isFull = usedTe >= totalTe; + const labelsTop = App.labelsPosition === 'top'; + + // Abgang-Labels aus Connections (output_label + Kabeltyp) generieren + let labelsHtml = `
`; + carrierEquipment.forEach(eq => { + const widthTe = parseInt(eq.width_te) || 1; + const posTe = parseInt(eq.position_te) || 0; + const gridCol = posTe > 0 + ? `grid-column: ${posTe} / span ${widthTe}` + : `grid-column: span ${widthTe}`; + // Abgang aus equipment_connection (fk_target IS NULL) + const output = App.outputs ? App.outputs.find(o => o.fk_source == eq.id) : null; + labelsHtml += `
`; + if (output && output.output_label) { + // Kabelinfo zusammenbauen (wie Website) + let cableInfo = ''; + if (output.medium_type) cableInfo = output.medium_type; + if (output.medium_spec) cableInfo += ' ' + output.medium_spec; + labelsHtml += ``; + labelsHtml += escapeHtml(output.output_label); + if (cableInfo) labelsHtml += `
${escapeHtml(cableInfo.trim())}`; + labelsHtml += `
`; + } + labelsHtml += `
`; + }); + labelsHtml += `
`; + html += ` -
+
${escapeHtml(carrier.label || 'Hutschiene')} - ${carrier.total_te || 12} TE + ${usedTe}/${totalTe} TE
+ `; + + // Labels oben + if (labelsTop) html += labelsHtml; + + html += `
+
`; carrierEquipment.forEach(eq => { const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); - const typeLabel = type ? (type.label_short || type.ref) : '?'; - const fieldVals = eq.field_values ? (typeof eq.field_values === 'string' ? JSON.parse(eq.field_values) : eq.field_values) : {}; - const value = fieldVals.ampere ? fieldVals.ampere + 'A' : (fieldVals.characteristic || ''); + const widthTe = parseInt(eq.width_te) || 1; + const posTe = parseInt(eq.position_te) || 0; + + // Wie Website: Zeile 1 = Typ-Kurzname, Zeile 2 = Feldwerte, Zeile 3 = Bezeichnung + const typeLabel = type?.label_short || type?.ref || ''; + const blockColor = eq.block_color || type?.color || '#3498db'; + const eqLabel = eq.label || ''; + + // block_label kann = type_label_short sein wenn keine Feldwerte vorhanden + // Nur anzeigen wenn es echte Feldwerte sind (nicht gleich dem Typ-Kurznamen) + const blockFields = eq.block_label || ''; + const showBlockFields = blockFields && blockFields !== typeLabel && blockFields !== (type?.ref || ''); + + const gridCol = posTe > 0 + ? `grid-column: ${posTe} / span ${widthTe}` + : `grid-column: span ${widthTe}`; html += ` -
-
${escapeHtml(typeLabel)}
-
${escapeHtml(value)}
- ${eq.label ? '
' + escapeHtml(eq.label) + '
' : ''} +
+ ${escapeHtml(typeLabel)} + ${showBlockFields ? `${escapeHtml(blockFields)}` : ''} + ${escapeHtml(eqLabel)}
`; }); + html += `
`; + html += ` - -
-
+ `; + + html += `
`; + + // Labels unten + if (!labelsTop) html += labelsHtml; + + html += `
`; }); html += ` @@ -558,8 +798,7 @@ // Reset modal $('.type-btn').removeClass('selected'); - $('#step-type').addClass('active'); - $('#step-values').removeClass('active'); + $('#step-values').hide(); $('#equipment-label').val(''); $('#value-fields').html(''); @@ -573,14 +812,13 @@ App.selectedTypeId = $(this).data('type-id'); const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId); - // Show value step - $('#step-type').removeClass('active'); - $('#step-values').addClass('active'); + // Werte-Bereich einblenden + $('#step-values').show(); - // Build value fields based on type + // Felder basierend auf Typ aufbauen let html = ''; - // Quick select for common values + // Quick-Select für LS-Schalter if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) { html += '

Kennlinie + Ampere:

'; html += '
'; @@ -588,6 +826,7 @@ html += ``; }); html += '
'; + // Quick-Select für FI-Schalter } else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) { html += '

Ampere:

'; html += '
'; @@ -601,11 +840,27 @@ html += ``; }); html += '
'; + // Quick-Select für AFDD + } else if (type && type.ref?.includes('AFDD')) { + html += '

Ampere:

'; + html += '
'; + ['10', '13', '16', '20', '25', '32'].forEach(v => { + html += ``; + }); + html += '
'; + // Quick-Select für FI/LS-Kombi + } else if (type && type.ref?.includes('FILS')) { + html += '

Kennlinie + Ampere:

'; + html += '
'; + ['B10', 'B13', 'B16', 'B20', 'B25', 'B32'].forEach(v => { + html += ``; + }); + html += '
'; } $('#value-fields').html(html); - // Bind chip clicks + // Chip-Klick-Handler $('#value-fields .value-chip').on('click', function() { if ($(this).hasClass('chip-sens')) { $('.chip-sens').removeClass('selected'); @@ -614,6 +869,11 @@ } $(this).addClass('selected'); }); + + // Focus auf Label-Feld wenn keine Chips vorhanden + if (!html) { + $('#equipment-label').focus(); + } } async function handleSaveEquipment() { @@ -638,14 +898,37 @@ fieldValues.sensitivity = selectedSens.data('sens'); } - // Calculate position + // Nächste freie Position berechnen (Lücken berücksichtigen) const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId); - let nextPos = 1; + const carrier = App.carriers.find(c => c.id == App.currentCarrierId); + const totalTe = parseInt(carrier?.total_te) || 12; + const eqWidth = parseInt(type?.width_te) || 1; + + // Belegungsarray erstellen + const occupied = new Array(totalTe + 1).fill(false); carrierEquipment.forEach(e => { - const endPos = (parseInt(e.position_te) || 1) + (parseInt(e.width_te) || 1); - if (endPos > nextPos) nextPos = endPos; + const pos = parseInt(e.position_te) || 1; + const w = parseInt(e.width_te) || 1; + for (let i = pos; i < pos + w && i <= totalTe; i++) { + occupied[i] = true; + } }); + // Erste Lücke finden die breit genug ist + let nextPos = 0; + for (let i = 1; i <= totalTe - eqWidth + 1; i++) { + let fits = true; + for (let j = 0; j < eqWidth; j++) { + if (occupied[i + j]) { fits = false; break; } + } + if (fits) { nextPos = i; break; } + } + + if (nextPos === 0) { + showToast('Kein Platz frei', 'error'); + return; + } + const data = { action: 'create_equipment', carrier_id: App.currentCarrierId, @@ -671,9 +954,12 @@ field_values: fieldValues }); renderEditor(); - showToast('Automat angelegt'); + showToast('Automat angelegt', 'success'); + } else { + showToast(response.error || 'Fehler beim Speichern', 'error'); } } catch (err) { + showToast('Netzwerkfehler - wird offline gespeichert', 'warning'); queueOfflineAction(data); } } else { @@ -719,6 +1005,16 @@ } } + async function handleRefresh() { + // Zuerst Offline-Queue syncen falls vorhanden + if (App.offlineQueue.length && App.isOnline) { + await syncOfflineChanges(); + } + // Dann Daten neu laden + showToast('Aktualisiere...'); + await loadEditorData(); + } + async function syncOfflineChanges() { if (!App.offlineQueue.length) { showToast('Alles synchronisiert'); @@ -830,98 +1126,7 @@ }; } - // jQuery shorthand - function $(selector) { - if (typeof selector === 'string') { - const elements = document.querySelectorAll(selector); - return new ElementCollection(elements); - } - return new ElementCollection([selector]); - } - - class ElementCollection { - constructor(elements) { - this.elements = Array.from(elements); - this.length = this.elements.length; - } - - on(event, selectorOrHandler, handler) { - if (typeof selectorOrHandler === 'function') { - // Direct event - this.elements.forEach(el => el.addEventListener(event, selectorOrHandler)); - } else { - // Delegated event - this.elements.forEach(el => { - el.addEventListener(event, function(e) { - const target = e.target.closest(selectorOrHandler); - if (target && el.contains(target)) { - handler.call(target, e); - } - }); - }); - } - return this; - } - - addClass(className) { - this.elements.forEach(el => el.classList.add(className)); - return this; - } - - removeClass(className) { - this.elements.forEach(el => el.classList.remove(className)); - return this; - } - - hasClass(className) { - return this.elements[0]?.classList.contains(className); - } - - html(content) { - if (content === undefined) { - return this.elements[0]?.innerHTML; - } - this.elements.forEach(el => el.innerHTML = content); - return this; - } - - text(content) { - if (content === undefined) { - return this.elements[0]?.textContent; - } - this.elements.forEach(el => el.textContent = content); - return this; - } - - val(value) { - if (value === undefined) { - return this.elements[0]?.value; - } - this.elements.forEach(el => el.value = value); - return this; - } - - data(key) { - return this.elements[0]?.dataset[key]; - } - - find(selector) { - const found = []; - this.elements.forEach(el => { - found.push(...el.querySelectorAll(selector)); - }); - return new ElementCollection(found); - } - - closest(selector) { - return new ElementCollection([this.elements[0]?.closest(selector)].filter(Boolean)); - } - - focus() { - this.elements[0]?.focus(); - return this; - } - } + // jQuery wird als $ Parameter der IIFE übergeben // ============================================ // START @@ -929,4 +1134,4 @@ document.addEventListener('DOMContentLoaded', init); -})(); +})(jQuery); diff --git a/pwa.php b/pwa.php index 0aa767e..ea67405 100644 --- a/pwa.php +++ b/pwa.php @@ -42,8 +42,10 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db'); KundenKarte + +
@@ -123,6 +125,9 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');

Anlage

+