kundenkarte/pwa.php
data 619d14e8d5 feat(pwa): FI-Schutzgruppen, gebündelte Terminals, Terminal-Konfiguration
- Schutzgruppen-Zuordnung: Equipment kann FI/RCD zugeordnet werden
  - Farbliche Markierung der Schutzgruppen im Schaltplan
  - Dropdown zur Auswahl des Schutzgeräts im Equipment-Dialog
- Gebündelte Terminals: Multi-Phasen-Abgänge (E-Herd, Durchlauferhitzer)
  - "Alle bündeln" Option im Abgang-Dialog
  - Zentriertes Label über alle Terminals des Equipment
- Terminal-Anzahl aus terminals_config statt TE-Breite
  - Neozed 3F zeigt korrekt 3 statt 4 Terminals
  - Neue getTerminalCount() Hilfsfunktion
- Zuletzt bearbeitete Kunden (max. 5) auf Search-Screen
- Medium-Typen dynamisch aus DB mit Spezifikationen-Dropdown
- Terminal-Labels anklickbar zum direkten Bearbeiten
- Kontextmenü für leere Terminals (Input/Output Auswahl)
- Block-Label mit Einheiten (40A 30mA statt 40A30mA)
- Online-Status-Anzeige entfernt (funktionierte nicht zuverlässig)
- Service Worker v5.2: Versionierte Assets nicht cachen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 14:34:54 +01:00

379 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* KundenKarte PWA - Mobile Schaltschrank-Dokumentation
* Offline-fähig für Elektriker vor Ort
*/
// Kein Dolibarr-Login erforderlich - eigenes Token-System
if (!defined('NOLOGIN')) {
define('NOLOGIN', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
// Load Dolibarr environment
$res = 0;
if (!$res && file_exists("../main.inc.php")) {
$res = @include "../main.inc.php";
}
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res) {
die("Dolibarr konnte nicht geladen werden");
}
// Theme color from Dolibarr
$themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="<?php echo $themeColor; ?>">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="KundenKarte">
<title>KundenKarte</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="img/pwa-icon-192.png">
<link rel="apple-touch-icon" href="img/pwa-icon-192.png">
<link rel="stylesheet" href="css/pwa.css?v=4.4">
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
</head>
<body>
<div id="app" class="app">
<!-- Login Screen -->
<div id="screen-login" class="screen active">
<div class="login-container">
<div class="login-logo">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2v-4H6v-2h4V7h2v4h4v2h-4v4z"/>
</svg>
</div>
<h1 class="login-title">KundenKarte</h1>
<p class="login-subtitle">Schaltschrank-Dokumentation</p>
<form id="login-form" class="login-form">
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="login-user" autocomplete="username" required>
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="login-pass" autocomplete="current-password" required>
</div>
<button type="submit" class="btn btn-primary btn-large">
Anmelden
</button>
<p id="login-error" class="error-text"></p>
</form>
</div>
</div>
<!-- Kundensuche Screen -->
<div id="screen-search" class="screen">
<header class="header">
<h1>Kunde wählen</h1>
<button id="btn-logout" class="btn-icon" title="Abmelden">
<svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
</button>
</header>
<div class="search-container">
<div class="search-box">
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="search-customer" placeholder="Kunde suchen...">
</div>
</div>
<div id="customer-list" class="list">
<!-- Kunden werden hier geladen -->
</div>
<!-- Zuletzt bearbeitete Kunden -->
<div id="recent-customers" class="recent-section">
<h3 class="recent-title">Zuletzt bearbeitet</h3>
<div id="recent-list" class="list">
<!-- Wird per JS gefüllt -->
</div>
</div>
</div>
<!-- Anlagen Screen -->
<div id="screen-anlagen" class="screen">
<header class="header">
<button id="btn-back-search" class="btn-icon">
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1 id="customer-name">Kunde</h1>
<div class="header-spacer"></div>
</header>
<div id="anlagen-list" class="list anlagen-grid">
<!-- Anlagen werden hier geladen -->
</div>
</div>
<!-- Editor Screen -->
<div id="screen-editor" class="screen">
<header class="header">
<button id="btn-back-anlagen" class="btn-icon">
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1 id="anlage-name">Anlage</h1>
<button id="btn-sync" class="btn-icon sync-btn">
<svg viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
<span id="sync-badge" class="sync-badge hidden">0</span>
</button>
</header>
<div id="editor-content" class="editor-content">
<!-- Felder und Hutschienen -->
</div>
<div class="fab-container">
<button id="btn-add-panel" class="fab">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Feld</span>
</button>
</div>
</div>
<!-- Automat hinzufügen/bearbeiten Modal -->
<div id="modal-add-equipment" class="modal">
<div class="modal-content">
<!-- Schritt 1: Typ wählen -->
<div id="eq-step-type">
<div class="modal-header">
<h2 id="equipment-modal-title">Automat hinzufügen</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="step-label">Typ wählen:</p>
<div id="type-grid" class="type-grid">
<!-- Typen werden geladen -->
</div>
</div>
</div>
<!-- Schritt 2: Felder ausfüllen -->
<div id="eq-step-fields" class="hidden">
<div class="modal-header">
<button id="btn-eq-back" class="btn-icon btn-back-modal">
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h2 id="eq-fields-title">Werte</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div id="eq-dynamic-fields">
<!-- Dynamische Felder vom Server -->
</div>
<div class="form-group">
<label>Bezeichnung</label>
<input type="text" id="equipment-label" placeholder="Leer = automatisch (z.B. R1.3)">
</div>
<!-- FI/RCD-Schutz Zuordnung -->
<div id="eq-protection-fields" class="protection-section">
<div class="form-group">
<label><svg viewBox="0 0 24 24" class="icon-small"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg> Schutzgerät (FI/RCD)</label>
<select id="equipment-protection" class="form-select">
<option value="">-- Keins --</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-equipment" class="btn btn-danger hidden">Löschen</button>
<div class="modal-footer-right">
<button id="btn-cancel-equipment" class="btn btn-secondary">Abbrechen</button>
<button id="btn-save-equipment" class="btn btn-primary">Speichern</button>
</div>
</div>
</div>
</div>
</div>
<!-- Bestätigungsdialog -->
<div id="modal-confirm" class="modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2 id="confirm-title">Löschen?</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p id="confirm-message">Diesen Automaten wirklich löschen?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary modal-close">Abbrechen</button>
<button id="btn-confirm-ok" class="btn btn-danger">Löschen</button>
</div>
</div>
</div>
<!-- Verbindung (Abgang/Anschlusspunkt) Modal -->
<div id="modal-connection" class="modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2 id="connection-modal-title">Verbindung bearbeiten</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group form-group-grow">
<label>Typ</label>
<select id="conn-type" class="form-select">
<!-- Wird dynamisch befüllt -->
</select>
</div>
<div class="form-group form-group-color">
<label>Farbe</label>
<input type="color" id="conn-color" class="form-color" value="#3498db">
</div>
</div>
<div class="form-group">
<label>Bezeichnung</label>
<input type="text" id="conn-label" class="form-input" placeholder="z.B. Küche Steckdosen">
</div>
<!-- Anschlussseite: Immer sichtbar (Automaten haben keine feste Richtung) -->
<div id="conn-side-fields" class="form-group">
<label>Anschlussseite</label>
<div id="conn-side-grid" class="side-grid">
<button type="button" class="side-btn selected" data-side="bottom">&#9660; Unten</button>
<button type="button" class="side-btn" data-side="top">&#9650; Oben</button>
</div>
</div>
<!-- Bundle-Option: Nur bei Abgängen + breitem Equipment sichtbar -->
<div id="conn-bundle-fields" class="form-group hidden">
<label class="checkbox-label">
<input type="checkbox" id="conn-bundle-all">
<span>Alle Terminals belegen (Drehstrom-Verbraucher)</span>
</label>
<p class="hint-text">Für E-Herd, Durchlauferhitzer u.ä. ein Abgang belegt alle Klemmen</p>
</div>
<!-- Medium-Felder: Nur bei Abgängen sichtbar -->
<div id="conn-output-fields">
<div class="form-group">
<label>Kabeltyp</label>
<select id="conn-medium-type" class="form-select">
<option value="">-- Auswählen --</option>
<!-- Wird per JS gefüllt -->
</select>
</div>
<div class="form-group">
<label>Querschnitt</label>
<select id="conn-medium-spec" class="form-select">
<option value="">-- Zuerst Kabeltyp wählen --</option>
</select>
</div>
<div class="form-group">
<label>Länge</label>
<input type="text" id="conn-medium-length" class="form-input" placeholder="z.B. 5m">
</div>
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-connection" class="btn btn-danger hidden">Löschen</button>
<div class="modal-footer-right">
<button class="btn btn-secondary modal-close">Abbrechen</button>
<button id="btn-save-connection" class="btn btn-primary">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Hutschiene hinzufügen/bearbeiten Modal -->
<div id="modal-add-carrier" class="modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2 id="carrier-modal-title">Hutschiene</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="step-label">Größe wählen:</p>
<div class="te-grid">
<button class="te-btn" data-te="12">12 TE</button>
<button class="te-btn" data-te="18">18 TE</button>
<button class="te-btn" data-te="24">24 TE</button>
<button class="te-btn" data-te="36">36 TE</button>
</div>
<div class="form-group" style="margin-top:16px;">
<label>Bezeichnung (optional)</label>
<input type="text" id="carrier-label" placeholder="z.B. Hutschiene 1">
</div>
</div>
<div class="modal-footer">
<button id="btn-delete-carrier" class="btn btn-danger hidden">Löschen</button>
<div class="modal-footer-right">
<button id="btn-save-carrier" class="btn btn-primary">Hinzufügen</button>
</div>
</div>
</div>
</div>
<!-- Feld hinzufügen Modal -->
<div id="modal-add-panel" class="modal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2>Neues Feld</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Bezeichnung</label>
<input type="text" id="panel-label" placeholder="z.B. Feld 1">
</div>
</div>
<div class="modal-footer">
<div class="modal-footer-right">
<button class="btn btn-secondary modal-close">Abbrechen</button>
<button id="btn-save-panel" class="btn btn-primary">Hinzufügen</button>
</div>
</div>
</div>
</div>
<!-- Equipment Detail Bottom-Sheet -->
<div id="sheet-equipment-detail" class="bottom-sheet">
<div class="sheet-overlay"></div>
<div class="sheet-content">
<div class="sheet-handle"></div>
<div class="sheet-header">
<div id="detail-type-badge" class="detail-type-badge"></div>
<div class="sheet-header-text">
<h2 id="detail-title">Equipment</h2>
<p id="detail-type-name" class="detail-subtitle"></p>
</div>
</div>
<div id="detail-body" class="sheet-body">
<!-- Dynamischer Inhalt -->
</div>
<div class="sheet-footer">
<button id="btn-detail-edit" class="btn btn-primary">Bearbeiten</button>
<button id="btn-detail-close" class="btn btn-secondary">Schließen</button>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toast" class="toast"></div>
</div>
<script src="<?php echo DOL_URL_ROOT; ?>/includes/jquery/js/jquery.min.js"></script>
<script>
// Dolibarr URL für AJAX
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
</script>
<script src="js/pwa.js?v=4.4"></script>
</body>
</html>