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('
Kennlinie + Ampere:
'; html += 'Ampere:
'; html += 'Ampere:
'; + html += 'Kennlinie + Ampere:
'; + html += 'Typ wählen:
-Typ wählen:
+Werte:
+ +