- 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>
415 lines
15 KiB
PHP
Executable file
415 lines
15 KiB
PHP
Executable file
<?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">×</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">×</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">×</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">×</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">▼ Unten</button>
|
||
<button type="button" class="side-btn" data-side="top">▲ 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">×</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">×</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>
|