feat(pwa): Kontakt-Adressen, Grid-Layout, Abgang-Labels, jQuery
- Kontakt-Adressen als aufklappbare Gruppen in Anlagen-Übersicht - Equipment-Blöcke als CSS Grid (TE-basiert) statt Flex-Wrap - Abgang-Labels (Outputs) über/unter Automaten, Toggle-Button - jQuery statt eigener ElementCollection, aus Dolibarr geladen - Design-System auf Dolibarr Dark Theme Variablen umgestellt - Session-State-Wiederherstellung bei Refresh - Browser-History Support (Hardware-Zurück) - Quick-Select erweitert: AFDD, FI/LS-Kombi - Intelligente Positionsberechnung mit Lücken-Erkennung - Hutschiene zeigt belegt/gesamt TE - $user->getrights() nach Token-Validierung - Doku aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
844e6060c6
commit
6e88f0eb87
7 changed files with 1009 additions and 362 deletions
47
CLAUDE.md
47
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.
|
Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentation vor Ort.
|
||||||
|
|
||||||
### Dateien
|
### 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)
|
- `pwa_auth.php` - Token-basierte Authentifizierung (15 Tage gültig)
|
||||||
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
|
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
|
||||||
- `js/pwa.js` - Komplette App-Logik (vanilla JS, kein jQuery)
|
- `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter)
|
||||||
- `css/pwa.css` - Mobile-First Dark Mode Design
|
- `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen
|
||||||
- `sw.js` - Service Worker für Offline-Cache
|
- `sw.js` - Service Worker für Offline-Cache (v1.8)
|
||||||
- `manifest.json` - Web App Manifest für Installation
|
- `manifest.json` - Web App Manifest für Installation
|
||||||
|
|
||||||
### Workflow
|
### Workflow
|
||||||
1. Login mit Dolibarr-Credentials → Token wird lokal gespeichert
|
1. Login mit Dolibarr-Credentials → Token wird lokal gespeichert
|
||||||
2. Kunde suchen → Anlagen werden gecached
|
2. Kunde suchen → Anlagen werden gecached
|
||||||
3. Anlage mit Schaltplan-Editor auswählen → Daten werden gecached
|
3. Kontakt-Adressen (Gebäude/Standorte) aufklappen → Anlagen pro Adresse
|
||||||
4. Offline arbeiten: Felder, Hutschienen, Automaten hinzufügen
|
4. Anlage mit Schaltplan-Editor auswählen → Daten werden gecached
|
||||||
5. Änderungen werden in lokaler Queue gespeichert
|
5. Offline arbeiten: Hutschienen, Automaten hinzufügen
|
||||||
6. Bei Internetverbindung: Automatische Synchronisierung
|
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
|
### Token-Authentifizierung
|
||||||
- Tokens enthalten: user_id, login, created, expires, hash
|
- Tokens enthalten: user_id, login, created, expires, hash
|
||||||
- Hash = MD5(user_id + login + MAIN_SECURITY_SALT)
|
- Hash = MD5(user_id + login + MAIN_SECURITY_SALT)
|
||||||
- Gültigkeit: 15 Tage
|
- Gültigkeit: 15 Tage
|
||||||
- Gespeichert in localStorage
|
- Gespeichert in localStorage
|
||||||
|
- `$user->getrights()` wird nach Token-Validierung aufgerufen
|
||||||
|
|
||||||
### Offline-Sync
|
### Offline-Sync
|
||||||
- Alle Änderungen werden in `offlineQueue` (localStorage) gespeichert
|
- Alle Änderungen werden in `offlineQueue` (localStorage) gespeichert
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ if ($tokenData['hash'] !== $expectedHash) {
|
||||||
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||||
$user = new User($db);
|
$user = new User($db);
|
||||||
$user->fetch($tokenData['user_id']);
|
$user->fetch($tokenData['user_id']);
|
||||||
|
$user->getrights();
|
||||||
|
|
||||||
if ($user->id <= 0 || $user->statut != 1) {
|
if ($user->id <= 0 || $user->statut != 1) {
|
||||||
echo json_encode(array('success' => false, 'error' => 'Benutzer nicht mehr aktiv'));
|
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/equipmentcarrier.class.php';
|
||||||
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.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/equipmenttype.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php';
|
||||||
|
|
||||||
$action = GETPOST('action', 'aZ09');
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
|
@ -117,7 +119,7 @@ switch ($action) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// GET ANLAGEN FOR CUSTOMER
|
// GET ANLAGEN FOR CUSTOMER (inkl. Kontakt-Adressen)
|
||||||
// ============================================
|
// ============================================
|
||||||
case 'get_anlagen':
|
case 'get_anlagen':
|
||||||
$customerId = GETPOSTINT('customer_id');
|
$customerId = GETPOSTINT('customer_id');
|
||||||
|
|
@ -126,18 +128,70 @@ switch ($action) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Root-Anlagen ohne Kontaktzuweisung (Kunden-Ebene)
|
||||||
$anlage = new Anlage($db);
|
$anlage = new Anlage($db);
|
||||||
$anlagen = $anlage->fetchAll('ASC', 'label', 0, 0, array('fk_soc' => $customerId));
|
$anlagen = $anlage->fetchChildren(0, $customerId);
|
||||||
|
|
||||||
$result = array();
|
$result = array();
|
||||||
if (is_array($anlagen)) {
|
|
||||||
foreach ($anlagen as $a) {
|
foreach ($anlagen as $a) {
|
||||||
$result[] = array(
|
$result[] = array(
|
||||||
'id' => $a->id,
|
'id' => $a->id,
|
||||||
'label' => $a->label,
|
'label' => $a->label,
|
||||||
|
'type' => $a->type_label,
|
||||||
'has_editor' => !empty($a->schematic_editor_enabled)
|
'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['success'] = true;
|
||||||
|
|
@ -195,11 +249,37 @@ switch ($action) {
|
||||||
'label' => $eq->label,
|
'label' => $eq->label,
|
||||||
'position_te' => $eq->position_te,
|
'position_te' => $eq->position_te,
|
||||||
'width_te' => $eq->width_te,
|
'width_te' => $eq->width_te,
|
||||||
|
'block_label' => $eq->getBlockLabel(),
|
||||||
|
'block_color' => $eq->getBlockColor(),
|
||||||
'field_values' => $eq->getFieldValues()
|
'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
|
// Load equipment types
|
||||||
$eqType = new EquipmentType($db);
|
$eqType = new EquipmentType($db);
|
||||||
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
|
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
|
||||||
|
|
@ -219,6 +299,7 @@ switch ($action) {
|
||||||
$response['panels'] = $panelsData;
|
$response['panels'] = $panelsData;
|
||||||
$response['carriers'] = $carriersData;
|
$response['carriers'] = $carriersData;
|
||||||
$response['equipment'] = $equipmentData;
|
$response['equipment'] = $equipmentData;
|
||||||
|
$response['outputs'] = $outputsData;
|
||||||
$response['types'] = $typesData;
|
$response['types'] = $typesData;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
684
css/pwa.css
684
css/pwa.css
File diff suppressed because it is too large
Load diff
493
js/pwa.js
493
js/pwa.js
|
|
@ -3,7 +3,7 @@
|
||||||
* Offline-First App für Elektriker
|
* Offline-First App für Elektriker
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function($) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
carriers: [],
|
carriers: [],
|
||||||
equipment: [],
|
equipment: [],
|
||||||
equipmentTypes: [],
|
equipmentTypes: [],
|
||||||
|
outputs: [],
|
||||||
|
|
||||||
// Offline queue
|
// Offline queue
|
||||||
offlineQueue: [],
|
offlineQueue: [],
|
||||||
|
|
@ -34,6 +35,9 @@
|
||||||
// Current modal state
|
// Current modal state
|
||||||
currentCarrierId: null,
|
currentCarrierId: null,
|
||||||
selectedTypeId: null,
|
selectedTypeId: null,
|
||||||
|
|
||||||
|
// Abgang-Labels: 'top' (Standard, wie echtes Panel) oder 'bottom'
|
||||||
|
labelsPosition: localStorage.getItem('kundenkarte_labels_pos') || 'top',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -66,8 +70,38 @@
|
||||||
if (storedToken && storedUser) {
|
if (storedToken && storedUser) {
|
||||||
App.token = storedToken;
|
App.token = storedToken;
|
||||||
App.user = JSON.parse(storedUser);
|
App.user = JSON.parse(storedUser);
|
||||||
|
|
||||||
|
// 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');
|
showScreen('search');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
showScreen('search');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialen History-State setzen
|
||||||
|
history.replaceState({ screen: $('.screen.active').attr('id')?.replace('screen-', '') || 'login' }, '');
|
||||||
|
|
||||||
// Load offline queue
|
// Load offline queue
|
||||||
const storedQueue = localStorage.getItem('kundenkarte_offline_queue');
|
const storedQueue = localStorage.getItem('kundenkarte_offline_queue');
|
||||||
|
|
@ -90,8 +124,19 @@
|
||||||
$('#btn-logout').on('click', handleLogout);
|
$('#btn-logout').on('click', handleLogout);
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
$('#btn-back-search').on('click', () => showScreen('search'));
|
$('#btn-back-search').on('click', () => history.back());
|
||||||
$('#btn-back-anlagen').on('click', () => showScreen('anlagen'));
|
$('#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
|
||||||
$('#search-customer').on('input', debounce(handleSearch, 300));
|
$('#search-customer').on('input', debounce(handleSearch, 300));
|
||||||
|
|
@ -99,6 +144,7 @@
|
||||||
// Customer/Anlage selection
|
// Customer/Anlage selection
|
||||||
$('#customer-list').on('click', '.list-item', handleCustomerSelect);
|
$('#customer-list').on('click', '.list-item', handleCustomerSelect);
|
||||||
$('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect);
|
$('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect);
|
||||||
|
$('#anlagen-list').on('click', '.contact-group-header', handleContactGroupClick);
|
||||||
|
|
||||||
// Editor actions
|
// Editor actions
|
||||||
$('#btn-add-panel').on('click', () => openModal('add-panel'));
|
$('#btn-add-panel').on('click', () => openModal('add-panel'));
|
||||||
|
|
@ -127,7 +173,18 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync button
|
// 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() {
|
function handleLogout() {
|
||||||
App.token = null;
|
App.token = null;
|
||||||
App.user = null;
|
App.user = null;
|
||||||
|
App.customerId = null;
|
||||||
|
App.customerName = '';
|
||||||
|
App.anlageId = null;
|
||||||
|
App.anlageName = '';
|
||||||
localStorage.removeItem('kundenkarte_pwa_token');
|
localStorage.removeItem('kundenkarte_pwa_token');
|
||||||
localStorage.removeItem('kundenkarte_pwa_user');
|
localStorage.removeItem('kundenkarte_pwa_user');
|
||||||
|
sessionStorage.removeItem('kundenkarte_pwa_state');
|
||||||
showScreen('login');
|
showScreen('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,14 +241,62 @@
|
||||||
// SCREENS
|
// SCREENS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
function showScreen(name) {
|
function showScreen(name, skipHistory) {
|
||||||
$('.screen').removeClass('active');
|
$('.screen').removeClass('active');
|
||||||
$('#screen-' + name).addClass('active');
|
$('#screen-' + name).addClass('active');
|
||||||
|
|
||||||
// Load data if needed
|
// Browser-History für Zurück-Button
|
||||||
if (name === 'search') {
|
if (!skipHistory) {
|
||||||
$('#search-customer').val('').focus();
|
history.pushState({ screen: name }, '', '#' + name);
|
||||||
$('#customer-list').html('<div class="list-empty">Suchbegriff eingeben...</div>');
|
}
|
||||||
|
|
||||||
|
// 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('<div class="loading-container"><div class="spinner"></div></div>');
|
||||||
|
|
||||||
|
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('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||||||
|
}
|
||||||
|
} 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('<div class="list-empty">Fehler beim Laden</div>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,10 +375,13 @@
|
||||||
customer_id: id
|
customer_id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success && response.anlagen) {
|
if (response.success) {
|
||||||
renderAnlagenList(response.anlagen);
|
renderAnlagenList(response.anlagen, response.contacts || []);
|
||||||
// Cache for offline
|
// Cache für Offline
|
||||||
localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify(response.anlagen));
|
localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify({
|
||||||
|
anlagen: response.anlagen,
|
||||||
|
contacts: response.contacts || []
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +389,8 @@
|
||||||
// Try cached
|
// Try cached
|
||||||
const cached = localStorage.getItem('kundenkarte_anlagen_' + id);
|
const cached = localStorage.getItem('kundenkarte_anlagen_' + id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
renderAnlagenList(JSON.parse(cached));
|
const data = JSON.parse(cached);
|
||||||
|
renderAnlagenList(data.anlagen || data, data.contacts || []);
|
||||||
showToast('Offline - Zeige gecachte Daten', 'warning');
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
||||||
} else {
|
} else {
|
||||||
$('#anlagen-list').html('<div class="list-empty">Fehler beim Laden</div>');
|
$('#anlagen-list').html('<div class="list-empty">Fehler beim Laden</div>');
|
||||||
|
|
@ -284,28 +398,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAnlagenList(anlagen) {
|
function renderAnlagenList(anlagen, contacts) {
|
||||||
// Filter nur Anlagen mit Editor
|
let html = '';
|
||||||
const withEditor = anlagen.filter(a => a.has_editor);
|
|
||||||
|
|
||||||
if (!withEditor.length) {
|
// Kunden-Anlagen (ohne Kontaktzuweisung)
|
||||||
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen mit Schaltplan-Editor</div>');
|
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 += `
|
||||||
|
<div class="contact-group" data-contact-id="${c.id}" data-customer-id="${App.customerId}">
|
||||||
|
<div class="contact-group-header">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
|
||||||
|
<div>
|
||||||
|
<div class="contact-group-name">${escapeHtml(c.name)}</div>
|
||||||
|
${subtitle ? '<div class="contact-group-address">' + escapeHtml(subtitle) + '</div>' : ''}
|
||||||
|
</div>
|
||||||
|
<span class="contact-group-count">${c.anlage_count}</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-anlagen-list"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
$('#anlagen-list').html(html);
|
||||||
withEditor.forEach(a => {
|
}
|
||||||
html += `
|
|
||||||
|
function renderAnlageCard(a) {
|
||||||
|
return `
|
||||||
<div class="anlage-card" data-id="${a.id}">
|
<div class="anlage-card" data-id="${a.id}">
|
||||||
<div class="anlage-card-icon">
|
<div class="anlage-card-icon">
|
||||||
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
|
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
|
||||||
|
${a.type ? '<div class="anlage-card-type">' + escapeHtml(a.type) + '</div>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
|
||||||
|
|
||||||
$('#anlagen-list').html(html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAnlageSelect() {
|
async function handleAnlageSelect() {
|
||||||
|
|
@ -320,6 +460,46 @@
|
||||||
await loadEditorData();
|
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('<div class="loading-container"><div class="spinner small"></div></div>');
|
||||||
|
|
||||||
|
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('<div class="list-empty small">Keine Anlagen</div>');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$list.html('<div class="list-empty small">Fehler beim Laden</div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// EDITOR
|
// EDITOR
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -338,13 +518,15 @@
|
||||||
App.carriers = response.carriers || [];
|
App.carriers = response.carriers || [];
|
||||||
App.equipment = response.equipment || [];
|
App.equipment = response.equipment || [];
|
||||||
App.equipmentTypes = response.types || [];
|
App.equipmentTypes = response.types || [];
|
||||||
|
App.outputs = response.outputs || [];
|
||||||
|
|
||||||
// Cache for offline
|
// Cache for offline
|
||||||
localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({
|
localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({
|
||||||
panels: App.panels,
|
panels: App.panels,
|
||||||
carriers: App.carriers,
|
carriers: App.carriers,
|
||||||
equipment: App.equipment,
|
equipment: App.equipment,
|
||||||
types: App.equipmentTypes
|
types: App.equipmentTypes,
|
||||||
|
outputs: App.outputs
|
||||||
}));
|
}));
|
||||||
|
|
||||||
renderEditor();
|
renderEditor();
|
||||||
|
|
@ -358,6 +540,7 @@
|
||||||
App.carriers = data.carriers || [];
|
App.carriers = data.carriers || [];
|
||||||
App.equipment = data.equipment || [];
|
App.equipment = data.equipment || [];
|
||||||
App.equipmentTypes = data.types || [];
|
App.equipmentTypes = data.types || [];
|
||||||
|
App.outputs = data.outputs || [];
|
||||||
renderEditor();
|
renderEditor();
|
||||||
showToast('Offline - Zeige gecachte Daten', 'warning');
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -389,37 +572,94 @@
|
||||||
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrier.id);
|
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrier.id);
|
||||||
carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0));
|
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 = `<div class="carrier-labels" style="grid-template-columns: repeat(${totalTe}, 1fr)">`;
|
||||||
|
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 += `<div class="carrier-label-cell" style="${gridCol}">`;
|
||||||
|
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 += `<span class="carrier-label-text">`;
|
||||||
|
labelsHtml += escapeHtml(output.output_label);
|
||||||
|
if (cableInfo) labelsHtml += `<br><small class="cable-info">${escapeHtml(cableInfo.trim())}</small>`;
|
||||||
|
labelsHtml += `</span>`;
|
||||||
|
}
|
||||||
|
labelsHtml += `</div>`;
|
||||||
|
});
|
||||||
|
labelsHtml += `</div>`;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="carrier-item" data-carrier-id="${carrier.id}">
|
<div class="carrier-item ${labelsTop ? 'labels-top' : 'labels-bottom'}" data-carrier-id="${carrier.id}">
|
||||||
<div class="carrier-header">
|
<div class="carrier-header">
|
||||||
<span class="carrier-label">${escapeHtml(carrier.label || 'Hutschiene')}</span>
|
<span class="carrier-label">${escapeHtml(carrier.label || 'Hutschiene')}</span>
|
||||||
<span class="carrier-te">${carrier.total_te || 12} TE</span>
|
<span class="carrier-te">${usedTe}/${totalTe} TE</span>
|
||||||
</div>
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Labels oben
|
||||||
|
if (labelsTop) html += labelsHtml;
|
||||||
|
|
||||||
|
html += `
|
||||||
<div class="carrier-body">
|
<div class="carrier-body">
|
||||||
|
<div class="carrier-grid" style="grid-template-columns: repeat(${totalTe}, 1fr)">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
carrierEquipment.forEach(eq => {
|
carrierEquipment.forEach(eq => {
|
||||||
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
|
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
|
||||||
const typeLabel = type ? (type.label_short || type.ref) : '?';
|
const widthTe = parseInt(eq.width_te) || 1;
|
||||||
const fieldVals = eq.field_values ? (typeof eq.field_values === 'string' ? JSON.parse(eq.field_values) : eq.field_values) : {};
|
const posTe = parseInt(eq.position_te) || 0;
|
||||||
const value = fieldVals.ampere ? fieldVals.ampere + 'A' : (fieldVals.characteristic || '');
|
|
||||||
|
// 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 += `
|
html += `
|
||||||
<div class="equipment-block" data-equipment-id="${eq.id}" style="background:${type?.color || '#3498db'}">
|
<div class="equipment-block" data-equipment-id="${eq.id}" style="background:${blockColor}; ${gridCol}">
|
||||||
<div class="equipment-block-type">${escapeHtml(typeLabel)}</div>
|
<span class="equipment-block-type">${escapeHtml(typeLabel)}</span>
|
||||||
<div class="equipment-block-value">${escapeHtml(value)}</div>
|
${showBlockFields ? `<span class="equipment-block-value">${escapeHtml(blockFields)}</span>` : ''}
|
||||||
${eq.label ? '<div class="equipment-block-label">' + escapeHtml(eq.label) + '</div>' : ''}
|
<span class="equipment-block-label">${escapeHtml(eqLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<button class="btn-add-equipment" data-carrier-id="${carrier.id}">
|
<button class="btn-add-equipment${isFull ? ' disabled' : ''}" data-carrier-id="${carrier.id}"${isFull ? ' disabled' : ''}>
|
||||||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
// Labels unten
|
||||||
|
if (!labelsTop) html += labelsHtml;
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
|
|
@ -558,8 +798,7 @@
|
||||||
|
|
||||||
// Reset modal
|
// Reset modal
|
||||||
$('.type-btn').removeClass('selected');
|
$('.type-btn').removeClass('selected');
|
||||||
$('#step-type').addClass('active');
|
$('#step-values').hide();
|
||||||
$('#step-values').removeClass('active');
|
|
||||||
$('#equipment-label').val('');
|
$('#equipment-label').val('');
|
||||||
$('#value-fields').html('');
|
$('#value-fields').html('');
|
||||||
|
|
||||||
|
|
@ -573,14 +812,13 @@
|
||||||
App.selectedTypeId = $(this).data('type-id');
|
App.selectedTypeId = $(this).data('type-id');
|
||||||
const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId);
|
const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId);
|
||||||
|
|
||||||
// Show value step
|
// Werte-Bereich einblenden
|
||||||
$('#step-type').removeClass('active');
|
$('#step-values').show();
|
||||||
$('#step-values').addClass('active');
|
|
||||||
|
|
||||||
// Build value fields based on type
|
// Felder basierend auf Typ aufbauen
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
// Quick select for common values
|
// Quick-Select für LS-Schalter
|
||||||
if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) {
|
if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) {
|
||||||
html += '<p class="step-label">Kennlinie + Ampere:</p>';
|
html += '<p class="step-label">Kennlinie + Ampere:</p>';
|
||||||
html += '<div class="value-quick">';
|
html += '<div class="value-quick">';
|
||||||
|
|
@ -588,6 +826,7 @@
|
||||||
html += `<button type="button" class="value-chip" data-char="${v[0]}" data-amp="${v.slice(1)}">${v}</button>`;
|
html += `<button type="button" class="value-chip" data-char="${v[0]}" data-amp="${v.slice(1)}">${v}</button>`;
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
// Quick-Select für FI-Schalter
|
||||||
} else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) {
|
} else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) {
|
||||||
html += '<p class="step-label">Ampere:</p>';
|
html += '<p class="step-label">Ampere:</p>';
|
||||||
html += '<div class="value-quick">';
|
html += '<div class="value-quick">';
|
||||||
|
|
@ -601,11 +840,27 @@
|
||||||
html += `<button type="button" class="value-chip chip-sens" data-sens="${v}">${v}mA</button>`;
|
html += `<button type="button" class="value-chip chip-sens" data-sens="${v}">${v}mA</button>`;
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
// Quick-Select für AFDD
|
||||||
|
} else if (type && type.ref?.includes('AFDD')) {
|
||||||
|
html += '<p class="step-label">Ampere:</p>';
|
||||||
|
html += '<div class="value-quick">';
|
||||||
|
['10', '13', '16', '20', '25', '32'].forEach(v => {
|
||||||
|
html += `<button type="button" class="value-chip" data-amp="${v}">${v}A</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
// Quick-Select für FI/LS-Kombi
|
||||||
|
} else if (type && type.ref?.includes('FILS')) {
|
||||||
|
html += '<p class="step-label">Kennlinie + Ampere:</p>';
|
||||||
|
html += '<div class="value-quick">';
|
||||||
|
['B10', 'B13', 'B16', 'B20', 'B25', 'B32'].forEach(v => {
|
||||||
|
html += `<button type="button" class="value-chip" data-char="${v[0]}" data-amp="${v.slice(1)}">${v}</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#value-fields').html(html);
|
$('#value-fields').html(html);
|
||||||
|
|
||||||
// Bind chip clicks
|
// Chip-Klick-Handler
|
||||||
$('#value-fields .value-chip').on('click', function() {
|
$('#value-fields .value-chip').on('click', function() {
|
||||||
if ($(this).hasClass('chip-sens')) {
|
if ($(this).hasClass('chip-sens')) {
|
||||||
$('.chip-sens').removeClass('selected');
|
$('.chip-sens').removeClass('selected');
|
||||||
|
|
@ -614,6 +869,11 @@
|
||||||
}
|
}
|
||||||
$(this).addClass('selected');
|
$(this).addClass('selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Focus auf Label-Feld wenn keine Chips vorhanden
|
||||||
|
if (!html) {
|
||||||
|
$('#equipment-label').focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveEquipment() {
|
async function handleSaveEquipment() {
|
||||||
|
|
@ -638,14 +898,37 @@
|
||||||
fieldValues.sensitivity = selectedSens.data('sens');
|
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);
|
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 => {
|
carrierEquipment.forEach(e => {
|
||||||
const endPos = (parseInt(e.position_te) || 1) + (parseInt(e.width_te) || 1);
|
const pos = parseInt(e.position_te) || 1;
|
||||||
if (endPos > nextPos) nextPos = endPos;
|
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 = {
|
const data = {
|
||||||
action: 'create_equipment',
|
action: 'create_equipment',
|
||||||
carrier_id: App.currentCarrierId,
|
carrier_id: App.currentCarrierId,
|
||||||
|
|
@ -671,9 +954,12 @@
|
||||||
field_values: fieldValues
|
field_values: fieldValues
|
||||||
});
|
});
|
||||||
renderEditor();
|
renderEditor();
|
||||||
showToast('Automat angelegt');
|
showToast('Automat angelegt', 'success');
|
||||||
|
} else {
|
||||||
|
showToast(response.error || 'Fehler beim Speichern', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||||||
queueOfflineAction(data);
|
queueOfflineAction(data);
|
||||||
}
|
}
|
||||||
} else {
|
} 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() {
|
async function syncOfflineChanges() {
|
||||||
if (!App.offlineQueue.length) {
|
if (!App.offlineQueue.length) {
|
||||||
showToast('Alles synchronisiert');
|
showToast('Alles synchronisiert');
|
||||||
|
|
@ -830,98 +1126,7 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// jQuery shorthand
|
// jQuery wird als $ Parameter der IIFE übergeben
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// START
|
// START
|
||||||
|
|
@ -929,4 +1134,4 @@
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
})();
|
})(jQuery);
|
||||||
|
|
|
||||||
18
pwa.php
18
pwa.php
|
|
@ -42,8 +42,10 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
||||||
<meta name="apple-mobile-web-app-title" content="KundenKarte">
|
<meta name="apple-mobile-web-app-title" content="KundenKarte">
|
||||||
<title>KundenKarte</title>
|
<title>KundenKarte</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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="apple-touch-icon" href="img/pwa-icon-192.png">
|
||||||
<link rel="stylesheet" href="css/pwa.css">
|
<link rel="stylesheet" href="css/pwa.css">
|
||||||
|
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="app">
|
<div id="app" class="app">
|
||||||
|
|
@ -123,6 +125,9 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
||||||
<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>
|
<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>
|
</button>
|
||||||
<h1 id="anlage-name">Anlage</h1>
|
<h1 id="anlage-name">Anlage</h1>
|
||||||
|
<button id="btn-toggle-labels" class="btn-toggle-labels labels-top" title="Abgänge oben/unten">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M7 14l5-5 5 5z"/></svg>
|
||||||
|
</button>
|
||||||
<button id="btn-sync" class="btn-icon sync-btn">
|
<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>
|
<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>
|
<span id="sync-badge" class="sync-badge hidden">0</span>
|
||||||
|
|
@ -149,23 +154,19 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
||||||
<button class="modal-close">×</button>
|
<button class="modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- Step 1: Typ wählen -->
|
|
||||||
<div id="step-type" class="step active">
|
|
||||||
<p class="step-label">Typ wählen:</p>
|
<p class="step-label">Typ wählen:</p>
|
||||||
<div id="type-grid" class="type-grid">
|
<div id="type-grid" class="type-grid">
|
||||||
<!-- Typen werden geladen -->
|
<!-- Typen werden geladen -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 2: Werte eingeben -->
|
<!-- Werte + Abgang (erscheint nach Typ-Auswahl) -->
|
||||||
<div id="step-values" class="step">
|
<div id="step-values" class="step-values-area">
|
||||||
<p class="step-label">Werte:</p>
|
|
||||||
<div id="value-fields" class="value-fields">
|
<div id="value-fields" class="value-fields">
|
||||||
<!-- Dynamische Felder -->
|
<!-- Dynamische Quick-Select Chips -->
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Abgang / Beschriftung</label>
|
<label>Abgang / Beschriftung</label>
|
||||||
<input type="text" id="equipment-label" placeholder="z.B. Küche Licht">
|
<input type="text" id="equipment-label" placeholder="z.B. Küche Licht, Steckdose Bad">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,6 +229,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="<?php echo DOL_URL_ROOT; ?>/includes/jquery/js/jquery.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dolibarr URL für AJAX
|
// Dolibarr URL für AJAX
|
||||||
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
|
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ switch ($action) {
|
||||||
if ($result && $db->num_rows($result) > 0) {
|
if ($result && $db->num_rows($result) > 0) {
|
||||||
$obj = $db->fetch_object($result);
|
$obj = $db->fetch_object($result);
|
||||||
$userLogin->fetch($obj->rowid);
|
$userLogin->fetch($obj->rowid);
|
||||||
|
$userLogin->getrights();
|
||||||
|
|
||||||
// Check password
|
// Check password
|
||||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
|
||||||
|
|
|
||||||
7
sw.js
7
sw.js
|
|
@ -3,8 +3,8 @@
|
||||||
* Offline-First für Schaltschrank-Dokumentation
|
* Offline-First für Schaltschrank-Dokumentation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'kundenkarte-pwa-v1.0';
|
const CACHE_NAME = 'kundenkarte-pwa-v1.8';
|
||||||
const OFFLINE_CACHE = 'kundenkarte-offline-v1.0';
|
const OFFLINE_CACHE = 'kundenkarte-offline-v1.8';
|
||||||
|
|
||||||
// Statische Assets die immer gecached werden
|
// Statische Assets die immer gecached werden
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
|
|
@ -12,7 +12,8 @@ const STATIC_ASSETS = [
|
||||||
'css/pwa.css',
|
'css/pwa.css',
|
||||||
'js/pwa.js',
|
'js/pwa.js',
|
||||||
'img/pwa-icon-192.png',
|
'img/pwa-icon-192.png',
|
||||||
'img/pwa-icon-512.png'
|
'img/pwa-icon-512.png',
|
||||||
|
'../../../includes/jquery/js/jquery.min.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Install - Cache statische Assets
|
// Install - Cache statische Assets
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue