kundenkarte/pwa.php
data 71272fa425 fix(schematic): Terminal-Farbpropagierung, Auto-Naming, PWA-Abgänge
- buildTerminalPhaseMap: Schritt 1b - Leitungen mit expliziter Farbe als
  Startpunkte (nur Gerät→Gerät, keine Abgänge)
- buildTerminalPhaseMap: Block-Durchreichung (Top↔Bottom) entfernt
- buildTerminalPhaseMap: Junction-Verbindungen (Terminal→Leitung)
  bidirektional verarbeitet via _connectionById Index
- PWA: Abgangs-Rendering mit Index-Fallback wenn source_terminal_id fehlt
- PWA: Abgangs-Labels max-height 130px, min-height 30px
- Auto-Naming: EquipmentCarrier create/update → 'R' + count
- Auto-Naming: EquipmentPanel update → 'Feld ' + count
- pwa_api.php: Hardcoded Fallbacks 'Feld'/'Hutschiene' entfernt
- pwa.js: Hutschiene Auto-Naming dynamisch aus Panel-Carrier-Anzahl
- kundenkarte.js: Carrier-Dialog Placeholder 'z.B. R1 (automatisch)'
- SW Cache auf v12.5 hochgezählt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:57:58 +01:00

415 lines
15 KiB
PHP
Executable file
Raw Permalink 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=5.9">
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
<script>
// Einmaliger Cache-Reset (v12.4) — löscht alte Service Worker + Caches + Editor-Daten
(function() {
var REQUIRED_VERSION = 'v12.4';
if (localStorage.getItem('sw_version') !== REQUIRED_VERSION) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(regs) {
regs.forEach(function(r) { r.unregister(); });
});
}
if ('caches' in window) {
caches.keys().then(function(names) {
names.forEach(function(n) { caches.delete(n); });
});
}
// Gecachte Editor-Daten löschen (kundenkarte_data_*)
var keysToRemove = [];
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key && key.indexOf('kundenkarte_data_') === 0) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(function(k) { localStorage.removeItem(k); });
localStorage.setItem('sw_version', REQUIRED_VERSION);
setTimeout(function() { location.reload(true); }, 500);
}
})();
</script>
</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>
<button id="btn-toggle-wires" class="btn-icon" title="Leitungen ein-/ausblenden">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" class="wire-icon-off"/><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-2zM9 17l-4-4 1.4-1.4 2.6 2.6 6.6-6.6L17 9l-8 8z" class="wire-icon-on hidden"/></svg>
</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>
<div id="conn-location-fields" class="form-group">
<label>Räumlichkeit</label>
<input type="text" id="conn-location" class="form-input" placeholder="z.B. Küche, Bad OG, Keller">
</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=5.9"></script>
</body>
</html>