feat(pwa): Anlagen-Übersicht Redesign, TE-Lücken, Feld-Badges
- Anlagen-Screen: Kontakte/Adressen oben als vertikale Liste mit Chevron - Anlagen-Cards: Horizontales Layout (Icon + Titel + Pfeil), volle Breite - Feld-Badges aus Admin-Einstellungen (show_in_tree) auf Anlagen-Cards - Kunden-Adresse als Trennlabel bei Anlagen ohne Kontaktzuweisung - Back-Navigation Fix: Anlagen werden nachgeladen falls leer (Refresh→Back) - TE-Lücken-Berechnung: getMaxGap() für zusammenhängende freie Slots - Typ-Buttons gefiltert: Nur Typen die in verfügbare Lücke passen - Equipment-Blöcke: 80px Höhe, Sicherungsautomat-Optik - PHP 8.1: trim() null-safe mit ?? '' - Cache-Versionen synchronisiert auf v2.7 - equipmentconnection: source/target_terminal_id im UPDATE SQL ergänzt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6e88f0eb87
commit
da4ed40ad2
8 changed files with 2069 additions and 424 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -135,7 +135,7 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
|
|||
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
|
||||
- `js/pwa.js` - Komplette App-Logik (jQuery, als IIFE mit jQuery-Parameter)
|
||||
- `css/pwa.css` - Mobile-First Design, Dolibarr Dark Theme Variablen
|
||||
- `sw.js` - Service Worker für Offline-Cache (v1.8)
|
||||
- `sw.js` - Service Worker für Offline-Cache (v2.7)
|
||||
- `manifest.json` - Web App Manifest für Installation
|
||||
|
||||
### Workflow
|
||||
|
|
@ -154,28 +154,35 @@ Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentat
|
|||
- Header: sticky, primary-Farbe
|
||||
|
||||
### Kontakt-Adressen
|
||||
- `get_anlagen` API liefert jetzt `anlagen` (Kunden-Ebene) + `contacts` (Adressen)
|
||||
- Kontakt-Gruppen werden als aufklappbare Akkordeons dargestellt
|
||||
- `get_anlagen` API liefert `anlagen` (Kunden-Ebene) + `contacts` (Adressen)
|
||||
- Kontakt-Gruppen (Gebäude/Standorte) werden OBEN als vertikale Liste dargestellt
|
||||
- Chevron-Pfeil zeigt Aufklapp-Status, Anlagen werden bei Klick geladen
|
||||
- Kunden-Anlagen darunter mit Kunden-Adresse als Trennlabel
|
||||
- `get_contact_anlagen` API lädt Anlagen pro Kontakt-Adresse bei Bedarf
|
||||
- Kontakt-Adressen zeigen Name, Adresse, Ort und Anzahl Anlagen
|
||||
- Layout: Alles untereinander (Handy-optimiert), keine Grid-Spalten
|
||||
|
||||
### 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`
|
||||
- Equipment-Blöcke: 80px Höhe, Sicherungsautomat-Optik mit Border und Schatten
|
||||
- `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
|
||||
- Intelligente Positionsberechnung mit Lücken-Erkennung (getMaxGap)
|
||||
- +-Button disabled wenn Hutschiene voll, Typ-Buttons gefiltert nach verfügbarer Breite
|
||||
- Hutschiene zeigt belegt/gesamt TE
|
||||
|
||||
### 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
|
||||
- Popstate-Handler: Lädt Anlagen-Daten nach falls Liste leer (z.B. nach Refresh→Back)
|
||||
- Sync-Button lädt jetzt erst Offline-Queue, dann Daten neu
|
||||
|
||||
### Anlagen-Übersicht
|
||||
- Anlagen-Cards: Horizontales Layout (Icon links, Titel rechts), volle Breite
|
||||
- Einspaltiges vertikales Layout - optimiert fürs Handy
|
||||
|
||||
### Token-Authentifizierung
|
||||
- Tokens enthalten: user_id, login, created, expires, hash
|
||||
- Hash = MD5(user_id + login + MAIN_SECURITY_SALT)
|
||||
|
|
|
|||
533
ajax/pwa_api.php
533
ajax/pwa_api.php
|
|
@ -78,6 +78,7 @@ require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class
|
|||
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/mediumtype.class.php';
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
|
|
@ -128,18 +129,43 @@ switch ($action) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Feld-Metadaten laden (show_in_tree Felder für Anzeige)
|
||||
$fieldMeta = array();
|
||||
$sqlFields = "SELECT fk_anlage_type, field_code, field_label, tree_display_mode, badge_color, show_in_tree, field_type";
|
||||
$sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||
$sqlFields .= " WHERE active = 1 AND show_in_tree = 1";
|
||||
$sqlFields .= " ORDER BY position";
|
||||
$resFields = $db->query($sqlFields);
|
||||
if ($resFields) {
|
||||
while ($fObj = $db->fetch_object($resFields)) {
|
||||
if ($fObj->field_type === 'header') continue;
|
||||
$fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array(
|
||||
'label' => $fObj->field_label,
|
||||
'display' => $fObj->tree_display_mode ?: 'badge',
|
||||
'color' => $fObj->badge_color ?: '',
|
||||
);
|
||||
}
|
||||
$db->free($resFields);
|
||||
}
|
||||
|
||||
// Root-Anlagen ohne Kontaktzuweisung (Kunden-Ebene)
|
||||
$anlage = new Anlage($db);
|
||||
$anlagen = $anlage->fetchChildren(0, $customerId);
|
||||
|
||||
$result = array();
|
||||
foreach ($anlagen as $a) {
|
||||
$result[] = array(
|
||||
$item = array(
|
||||
'id' => $a->id,
|
||||
'label' => $a->label,
|
||||
'type' => $a->type_label,
|
||||
'has_editor' => !empty($a->schematic_editor_enabled)
|
||||
);
|
||||
// Feld-Badges hinzufügen
|
||||
$fields = pwaGetAnlageFields($a, $fieldMeta);
|
||||
if (!empty($fields)) {
|
||||
$item['fields'] = $fields;
|
||||
}
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
// Kontakt-Adressen mit Anlagen laden
|
||||
|
|
@ -181,17 +207,43 @@ switch ($action) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Feld-Metadaten laden falls nicht schon vorhanden
|
||||
if (!isset($fieldMeta)) {
|
||||
$fieldMeta = array();
|
||||
$sqlFields = "SELECT fk_anlage_type, field_code, field_label, tree_display_mode, badge_color, show_in_tree, field_type";
|
||||
$sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||
$sqlFields .= " WHERE active = 1 AND show_in_tree = 1";
|
||||
$sqlFields .= " ORDER BY position";
|
||||
$resFields = $db->query($sqlFields);
|
||||
if ($resFields) {
|
||||
while ($fObj = $db->fetch_object($resFields)) {
|
||||
if ($fObj->field_type === 'header') continue;
|
||||
$fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array(
|
||||
'label' => $fObj->field_label,
|
||||
'display' => $fObj->tree_display_mode ?: 'badge',
|
||||
'color' => $fObj->badge_color ?: '',
|
||||
);
|
||||
}
|
||||
$db->free($resFields);
|
||||
}
|
||||
}
|
||||
|
||||
$anlage = new Anlage($db);
|
||||
$anlagen = $anlage->fetchChildrenByContact(0, $customerId, $contactId);
|
||||
|
||||
$result = array();
|
||||
foreach ($anlagen as $a) {
|
||||
$result[] = array(
|
||||
$item = array(
|
||||
'id' => $a->id,
|
||||
'label' => $a->label,
|
||||
'type' => $a->type_label,
|
||||
'has_editor' => !empty($a->schematic_editor_enabled)
|
||||
);
|
||||
$fields = pwaGetAnlageFields($a, $fieldMeta);
|
||||
if (!empty($fields)) {
|
||||
$item['fields'] = $fields;
|
||||
}
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
$response['success'] = true;
|
||||
|
|
@ -256,11 +308,15 @@ switch ($action) {
|
|||
}
|
||||
}
|
||||
|
||||
// Equipment-Typen laden (benötigt für Terminal-Position-Auflösung)
|
||||
$eqType = new EquipmentType($db);
|
||||
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
|
||||
|
||||
// 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 = "SELECT rowid, fk_source, output_label, medium_type, medium_spec, medium_length, connection_type, color, source_terminal, source_terminal_id";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
|
||||
$sql .= " WHERE fk_source IN (".implode(',', $equipmentIds).")";
|
||||
$sql .= " AND fk_target IS NULL";
|
||||
|
|
@ -268,21 +324,86 @@ switch ($action) {
|
|||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
// Position bestimmen wie Website: source_terminal_id → Terminal-Config
|
||||
$isTop = false;
|
||||
if ($obj->source_terminal === 'top') {
|
||||
// PWA-erstellte Verbindung mit expliziter Top-Angabe
|
||||
$isTop = true;
|
||||
} elseif (!empty($obj->source_terminal_id)) {
|
||||
// Website-erstellte Verbindung: Terminal-Position aus Equipment-Typ ermitteln
|
||||
$termId = $obj->source_terminal_id;
|
||||
$eqTypeId = null;
|
||||
foreach ($equipmentData as $e) {
|
||||
if ($e['id'] == $obj->fk_source) {
|
||||
$eqTypeId = $e['fk_equipment_type'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($eqTypeId) {
|
||||
// Terminal-Config des Typs prüfen
|
||||
$termResolved = false;
|
||||
foreach ($types as $t) {
|
||||
if ($t->id == $eqTypeId && !empty($t->terminals_config)) {
|
||||
$config = json_decode($t->terminals_config, true);
|
||||
$terms = $config['terminals'] ?? ($config['inputs'] ?? []);
|
||||
if (!empty($config['terminals'])) {
|
||||
foreach ($config['terminals'] as $term) {
|
||||
if ($term['id'] === $termId) {
|
||||
$isTop = ($term['pos'] === 'top');
|
||||
$termResolved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback: t1=oben, t2=unten (Standard LS)
|
||||
if (!$termResolved) {
|
||||
$isTop = ($termId === 't1');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
'medium_length' => $obj->medium_length,
|
||||
'connection_type' => $obj->connection_type,
|
||||
'color' => $obj->color,
|
||||
'source_terminal_id' => $obj->source_terminal_id ?: '',
|
||||
'is_top' => $isTop
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load equipment types
|
||||
$eqType = new EquipmentType($db);
|
||||
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
|
||||
// Einspeisungen laden (Connections mit fk_source IS NULL = Inputs)
|
||||
$inputsData = array();
|
||||
if (!empty($equipmentData)) {
|
||||
$sql = "SELECT rowid, fk_target, output_label, connection_type, color";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
|
||||
$sql .= " WHERE fk_target IN (".implode(',', $equipmentIds).")";
|
||||
$sql .= " AND fk_source IS NULL";
|
||||
$sql .= " AND status = 1";
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$inputsData[] = array(
|
||||
'id' => $obj->rowid,
|
||||
'fk_target' => $obj->fk_target,
|
||||
'output_label' => $obj->output_label,
|
||||
'connection_type' => $obj->connection_type,
|
||||
'color' => $obj->color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Equipment-Typen für Response aufbereiten (bereits oben geladen)
|
||||
$typesData = array();
|
||||
foreach ($types as $t) {
|
||||
$typesData[] = array(
|
||||
|
|
@ -300,6 +421,7 @@ switch ($action) {
|
|||
$response['carriers'] = $carriersData;
|
||||
$response['equipment'] = $equipmentData;
|
||||
$response['outputs'] = $outputsData;
|
||||
$response['inputs'] = $inputsData;
|
||||
$response['types'] = $typesData;
|
||||
break;
|
||||
|
||||
|
|
@ -351,7 +473,15 @@ switch ($action) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Anlage-ID vom Panel holen (benötigt für Carrier-Erstellung)
|
||||
$panelObj = new EquipmentPanel($db);
|
||||
if ($panelObj->fetch($panelId) <= 0) {
|
||||
$response['error'] = 'Panel nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrier = new EquipmentCarrier($db);
|
||||
$carrier->fk_anlage = $panelObj->fk_anlage;
|
||||
$carrier->fk_panel = $panelId;
|
||||
$carrier->label = $label ?: 'Hutschiene';
|
||||
$carrier->total_te = $totalTe;
|
||||
|
|
@ -365,6 +495,70 @@ switch ($action) {
|
|||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// UPDATE CARRIER
|
||||
// ============================================
|
||||
case 'update_carrier':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrierId = GETPOSTINT('carrier_id');
|
||||
if ($carrierId <= 0) {
|
||||
$response['error'] = 'Keine Carrier-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrier = new EquipmentCarrier($db);
|
||||
if ($carrier->fetch($carrierId) <= 0) {
|
||||
$response['error'] = 'Hutschiene nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrier->label = GETPOST('label', 'alphanohtml') ?: $carrier->label;
|
||||
$newTe = GETPOSTINT('total_te');
|
||||
if ($newTe > 0) {
|
||||
$carrier->total_te = $newTe;
|
||||
}
|
||||
|
||||
$result = $carrier->update($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = $carrier->error ?: 'Fehler beim Aktualisieren';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// DELETE CARRIER
|
||||
// ============================================
|
||||
case 'delete_carrier':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrierId = GETPOSTINT('carrier_id');
|
||||
if ($carrierId <= 0) {
|
||||
$response['error'] = 'Keine Carrier-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$carrier = new EquipmentCarrier($db);
|
||||
if ($carrier->fetch($carrierId) <= 0) {
|
||||
$response['error'] = 'Hutschiene nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $carrier->delete($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = $carrier->error ?: 'Fehler beim Löschen';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// CREATE EQUIPMENT
|
||||
// ============================================
|
||||
|
|
@ -397,18 +591,343 @@ switch ($action) {
|
|||
$equipment->width_te = $eqType->width_te ?: 1;
|
||||
$equipment->field_values = $fieldValues;
|
||||
|
||||
// Bezeichnung automatisch generieren wenn leer (wie Website)
|
||||
if (empty(trim($equipment->label ?? ''))) {
|
||||
$carrier = new EquipmentCarrier($db);
|
||||
if ($carrier->fetch($carrierId) > 0) {
|
||||
$carrierLabel = $carrier->label ?: ('R'.$carrier->id);
|
||||
$posStart = $equipment->position_te;
|
||||
$posEnd = $posStart + $equipment->width_te - 1;
|
||||
if ($equipment->width_te > 1) {
|
||||
$equipment->label = $carrierLabel.'.'.$posStart.'-'.$posEnd;
|
||||
} else {
|
||||
$equipment->label = $carrierLabel.'.'.$posStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = $equipment->create($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
$response['equipment_id'] = $result;
|
||||
$response['label'] = $equipment->label;
|
||||
$response['block_label'] = $equipment->getBlockLabel();
|
||||
$response['block_color'] = $equipment->getBlockColor();
|
||||
} else {
|
||||
$response['error'] = $equipment->error ?: 'Fehler beim Anlegen';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// GET TYPE FIELDS (Felder pro Equipment-Typ)
|
||||
// ============================================
|
||||
case 'get_type_fields':
|
||||
$typeId = GETPOSTINT('type_id');
|
||||
$equipmentId = GETPOSTINT('equipment_id');
|
||||
|
||||
if ($typeId <= 0) {
|
||||
$response['error'] = 'Keine Typ-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$type = new EquipmentType($db);
|
||||
if ($type->fetch($typeId) <= 0) {
|
||||
$response['error'] = 'Typ nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$fields = $type->fetchFields(1);
|
||||
|
||||
// Bestehende Werte laden (bei Bearbeitung)
|
||||
$existingValues = array();
|
||||
if ($equipmentId > 0) {
|
||||
$eq = new Equipment($db);
|
||||
if ($eq->fetch($equipmentId) > 0) {
|
||||
$existingValues = $eq->getFieldValues();
|
||||
}
|
||||
}
|
||||
|
||||
$result = array();
|
||||
foreach ($fields as $field) {
|
||||
$value = isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : $field->field_default;
|
||||
$result[] = array(
|
||||
'code' => $field->field_code,
|
||||
'label' => $field->field_label,
|
||||
'type' => $field->field_type,
|
||||
'options' => $field->field_options,
|
||||
'required' => (int) $field->required,
|
||||
'show_on_block' => (int) $field->show_on_block,
|
||||
'value' => $value
|
||||
);
|
||||
}
|
||||
|
||||
$response['success'] = true;
|
||||
$response['fields'] = $result;
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// UPDATE EQUIPMENT
|
||||
// ============================================
|
||||
case 'update_equipment':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipmentId = GETPOSTINT('equipment_id');
|
||||
$label = GETPOST('label', 'alphanohtml');
|
||||
$fieldValues = GETPOST('field_values', 'nohtml');
|
||||
|
||||
if ($equipmentId <= 0) {
|
||||
$response['error'] = 'Keine Equipment-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipment = new Equipment($db);
|
||||
if ($equipment->fetch($equipmentId) <= 0) {
|
||||
$response['error'] = 'Automat nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipment->label = $label;
|
||||
$equipment->field_values = $fieldValues;
|
||||
|
||||
$result = $equipment->update($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
// Aktualisierte Daten zurückgeben
|
||||
$response['block_label'] = $equipment->getBlockLabel();
|
||||
$response['block_color'] = $equipment->getBlockColor();
|
||||
} else {
|
||||
$response['error'] = $equipment->error ?: 'Fehler beim Aktualisieren';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// DELETE EQUIPMENT
|
||||
// ============================================
|
||||
case 'delete_equipment':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipmentId = GETPOSTINT('equipment_id');
|
||||
|
||||
if ($equipmentId <= 0) {
|
||||
$response['error'] = 'Keine Equipment-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipment = new Equipment($db);
|
||||
if ($equipment->fetch($equipmentId) <= 0) {
|
||||
$response['error'] = 'Automat nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $equipment->delete($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = $equipment->error ?: 'Fehler beim Löschen';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// CREATE CONNECTION (Abgang oder Einspeisung)
|
||||
// ============================================
|
||||
case 'create_connection':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$equipmentId = GETPOSTINT('equipment_id');
|
||||
$direction = GETPOST('direction', 'alpha'); // 'output' oder 'input'
|
||||
$connectionType = GETPOST('connection_type', 'alphanohtml');
|
||||
$outputLabel = GETPOST('output_label', 'alphanohtml');
|
||||
$mediumType = GETPOST('medium_type', 'alphanohtml');
|
||||
$mediumSpec = GETPOST('medium_spec', 'alphanohtml');
|
||||
$mediumLength = GETPOST('medium_length', 'alphanohtml');
|
||||
$sourceTerminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output';
|
||||
$sourceTerminalId = GETPOST('source_terminal_id', 'alphanohtml');
|
||||
|
||||
if ($equipmentId <= 0) {
|
||||
$response['error'] = 'Keine Equipment-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
// Equipment prüfen und Carrier-ID ermitteln
|
||||
$eq = new Equipment($db);
|
||||
if ($eq->fetch($equipmentId) <= 0) {
|
||||
$response['error'] = 'Automat nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$conn = new EquipmentConnection($db);
|
||||
$conn->connection_type = $connectionType;
|
||||
$conn->color = GETPOST('color', 'alphanohtml');
|
||||
$conn->output_label = $outputLabel;
|
||||
$conn->fk_carrier = $eq->fk_carrier;
|
||||
|
||||
if ($direction === 'input') {
|
||||
// Einspeisung: fk_source = NULL, fk_target = Equipment
|
||||
$conn->fk_target = $equipmentId;
|
||||
$conn->fk_source = null;
|
||||
$conn->target_terminal = 'input';
|
||||
} else {
|
||||
// Abgang: fk_source = Equipment, fk_target = NULL
|
||||
$conn->fk_source = $equipmentId;
|
||||
$conn->fk_target = null;
|
||||
$conn->source_terminal = $sourceTerminal;
|
||||
$conn->source_terminal_id = $sourceTerminalId ?: ($sourceTerminal === 'top' ? 't1' : 't2');
|
||||
$conn->medium_type = $mediumType;
|
||||
$conn->medium_spec = $mediumSpec;
|
||||
$conn->medium_length = $mediumLength;
|
||||
}
|
||||
|
||||
$result = $conn->create($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
$response['connection_id'] = $result;
|
||||
} else {
|
||||
$response['error'] = $conn->error ?: 'Fehler beim Anlegen';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// UPDATE CONNECTION
|
||||
// ============================================
|
||||
case 'update_connection':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$connectionId = GETPOSTINT('connection_id');
|
||||
if ($connectionId <= 0) {
|
||||
$response['error'] = 'Keine Verbindungs-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$conn = new EquipmentConnection($db);
|
||||
if ($conn->fetch($connectionId) <= 0) {
|
||||
$response['error'] = 'Verbindung nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$conn->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||
$conn->color = GETPOST('color', 'alphanohtml');
|
||||
$conn->output_label = GETPOST('output_label', 'alphanohtml');
|
||||
$conn->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||
$conn->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||
$conn->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||
if (GETPOSTISSET('source_terminal')) {
|
||||
$conn->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: $conn->source_terminal;
|
||||
}
|
||||
if (GETPOSTISSET('source_terminal_id')) {
|
||||
$conn->source_terminal_id = GETPOST('source_terminal_id', 'alphanohtml') ?: $conn->source_terminal_id;
|
||||
}
|
||||
|
||||
$result = $conn->update($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = $conn->error ?: 'Fehler beim Aktualisieren';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// DELETE CONNECTION
|
||||
// ============================================
|
||||
case 'delete_connection':
|
||||
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||
$response['error'] = 'Keine Schreibberechtigung';
|
||||
break;
|
||||
}
|
||||
|
||||
$connectionId = GETPOSTINT('connection_id');
|
||||
if ($connectionId <= 0) {
|
||||
$response['error'] = 'Keine Verbindungs-ID';
|
||||
break;
|
||||
}
|
||||
|
||||
$conn = new EquipmentConnection($db);
|
||||
if ($conn->fetch($connectionId) <= 0) {
|
||||
$response['error'] = 'Verbindung nicht gefunden';
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $conn->delete($user);
|
||||
if ($result > 0) {
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = $conn->error ?: 'Fehler beim Löschen';
|
||||
}
|
||||
break;
|
||||
|
||||
// ============================================
|
||||
// GET MEDIUM TYPES (Kabeltypen aus DB)
|
||||
// ============================================
|
||||
case 'get_medium_types':
|
||||
$mediumType = new MediumType($db);
|
||||
$grouped = $mediumType->fetchGroupedByCategory(1); // System 1 = Elektro
|
||||
|
||||
$result = array();
|
||||
foreach ($grouped as $category => $types) {
|
||||
$catTypes = array();
|
||||
foreach ($types as $t) {
|
||||
$catTypes[] = array(
|
||||
'ref' => $t->ref,
|
||||
'label' => $t->label,
|
||||
'default_spec' => $t->default_spec,
|
||||
'available_specs' => $t->getAvailableSpecsArray(),
|
||||
);
|
||||
}
|
||||
$result[] = array(
|
||||
'category' => $category,
|
||||
'category_label' => $types[0]->getCategoryLabel(),
|
||||
'types' => $catTypes
|
||||
);
|
||||
}
|
||||
|
||||
$response['success'] = true;
|
||||
$response['groups'] = $result;
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['error'] = 'Unbekannte Aktion: ' . $action;
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
$db->close();
|
||||
|
||||
/**
|
||||
* Feld-Badges für eine Anlage aufbereiten (show_in_tree Felder)
|
||||
*
|
||||
* @param Anlage $anlage Anlage-Objekt
|
||||
* @param array $fieldMeta Feld-Metadaten [typeId][code] = {label, display, color}
|
||||
* @return array Array von Feld-Objekten mit label, value, color
|
||||
*/
|
||||
function pwaGetAnlageFields($anlage, $fieldMeta) {
|
||||
$result = array();
|
||||
$values = $anlage->getFieldValues();
|
||||
if (empty($values)) return $result;
|
||||
|
||||
$typeId = (int) $anlage->fk_anlage_type;
|
||||
$meta = isset($fieldMeta[$typeId]) ? $fieldMeta[$typeId] : array();
|
||||
if (empty($meta)) return $result;
|
||||
|
||||
foreach ($meta as $code => $fm) {
|
||||
$val = isset($values[$code]) ? $values[$code] : null;
|
||||
if ($val === '' || $val === null) continue;
|
||||
|
||||
$result[] = array(
|
||||
'label' => $fm['label'],
|
||||
'value' => $val,
|
||||
'color' => $fm['color'],
|
||||
'display' => $fm['display'],
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,8 +218,10 @@ class EquipmentConnection extends CommonObject
|
|||
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||
$sql .= " fk_source = ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL");
|
||||
$sql .= ", source_terminal = '".$this->db->escape($this->source_terminal ?: 'output')."'";
|
||||
$sql .= ", source_terminal_id = ".($this->source_terminal_id ? "'".$this->db->escape($this->source_terminal_id)."'" : "NULL");
|
||||
$sql .= ", fk_target = ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL");
|
||||
$sql .= ", target_terminal = '".$this->db->escape($this->target_terminal ?: 'input')."'";
|
||||
$sql .= ", target_terminal_id = ".($this->target_terminal_id ? "'".$this->db->escape($this->target_terminal_id)."'" : "NULL");
|
||||
$sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
|
||||
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||
$sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
|
||||
|
|
|
|||
570
css/pwa.css
570
css/pwa.css
|
|
@ -443,25 +443,27 @@ body {
|
|||
|
||||
.anlagen-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.anlagen-grid > .anlage-card {
|
||||
width: calc(50% - 5px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.anlagen-grid > .contact-group {
|
||||
width: 100%;
|
||||
/* Trennlabel Kunden-Adresse */
|
||||
.anlagen-section-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--colortextmuted);
|
||||
padding: 8px 4px 2px;
|
||||
border-top: 1px solid var(--colorborder);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.anlage-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 18px 14px;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
background: var(--colorbackline);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
|
|
@ -475,48 +477,84 @@ body {
|
|||
}
|
||||
|
||||
.anlage-card-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: var(--success);
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anlage-card-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.anlage-card-title {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.anlage-card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anlage-card-type {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--colortextmuted);
|
||||
margin-top: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.anlage-card-arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: var(--colortextmuted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anlage-card-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.anlage-field-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--colorbackinput);
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CONTACT GROUPS
|
||||
============================================ */
|
||||
|
||||
.contact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-group {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.contact-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: var(--colorbackline);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
|
|
@ -552,8 +590,12 @@ body {
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.contact-group-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contact-group-count {
|
||||
margin-left: auto;
|
||||
background: var(--colorbackinput);
|
||||
color: var(--colortextmuted);
|
||||
font-size: 12px;
|
||||
|
|
@ -562,35 +604,35 @@ body {
|
|||
border-radius: 10px;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-group-chevron {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: var(--colortextmuted);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.contact-group.expanded .contact-group-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.contact-anlagen-list {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
background: rgba(59, 60, 62, 0.4);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-top: none;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contact-group.expanded .contact-anlagen-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-anlagen-list .anlage-card {
|
||||
width: calc(50% - 4px);
|
||||
padding: 14px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.contact-anlagen-list .anlage-card-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.contact-anlagen-list .loading-container,
|
||||
|
|
@ -625,8 +667,9 @@ body {
|
|||
background: var(--colorbackline);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
margin: 0 auto 12px auto;
|
||||
overflow: hidden;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
|
|
@ -667,6 +710,7 @@ body {
|
|||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.carrier-label {
|
||||
|
|
@ -681,35 +725,35 @@ body {
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.carrier-body {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 3px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.carrier-grid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* Carrier-Content: EIN Grid für Terminals + Equipment + +-Button
|
||||
Spalten: repeat(totalTE, 1fr) auto
|
||||
Zeile 1: Input-Terminals (Einspeisungen)
|
||||
Zeile 2: Equipment-Blöcke + +-Button
|
||||
Zeile 3: Output-Terminals (Abgänge) */
|
||||
.carrier-content {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Equipment Block */
|
||||
/* Equipment Block (Zeile 2) - Sicherungsautomat-Optik */
|
||||
.equipment-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
height: 52px;
|
||||
padding: 2px 1px;
|
||||
height: 80px;
|
||||
padding: 4px 2px;
|
||||
background: var(--primary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.equipment-block:active {
|
||||
|
|
@ -724,10 +768,10 @@ body {
|
|||
}
|
||||
|
||||
.equipment-block-value {
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.equipment-block-label {
|
||||
|
|
@ -754,119 +798,103 @@ body {
|
|||
padding: 0 1px;
|
||||
}
|
||||
|
||||
/* Abgang-Labels (Zeile über/unter den Blöcken) */
|
||||
.carrier-labels {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 0 5px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.carrier-label-cell {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Labels oben: Text vertikal von unten nach oben */
|
||||
.labels-top .carrier-labels {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.labels-top .carrier-label-text {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
line-height: 1.3;
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.labels-top .carrier-label-text .cable-info {
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Labels unten: Text vertikal von oben nach unten */
|
||||
.labels-bottom .carrier-labels {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.labels-bottom .carrier-label-cell {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.labels-bottom .carrier-label-text {
|
||||
writing-mode: vertical-rl;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
line-height: 1.3;
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.labels-bottom .carrier-label-text .cable-info {
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Toggle-Button für Label-Position */
|
||||
.btn-toggle-labels {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
/* Terminal-Point = Einzelne klickbare Klemme
|
||||
Sitzt in derselben Grid-Spalte wie der zugehörige Equipment-Block */
|
||||
.terminal-point {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.15);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.3s;
|
||||
padding: 3px 2px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.btn-toggle-labels:active {
|
||||
background: rgba(255,255,255,0.3);
|
||||
.terminal-point.terminal-input {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.btn-toggle-labels svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: currentColor;
|
||||
.terminal-point.terminal-output {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
/* Pfeil nach oben = Labels oben */
|
||||
.btn-toggle-labels.labels-top svg {
|
||||
transform: rotate(0deg);
|
||||
.terminal-point:active {
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* Pfeil nach unten = Labels unten */
|
||||
.btn-toggle-labels.labels-bottom svg {
|
||||
/* Terminal-Punkt (Kreis für Inputs) */
|
||||
.terminal-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.terminal-dot.terminal-empty {
|
||||
background: transparent;
|
||||
border: 2px dashed rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
/* Terminal-Pfeil (für Outputs) */
|
||||
.terminal-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminal-arrow-down {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 10px solid var(--arrow-color, #888);
|
||||
}
|
||||
|
||||
.terminal-arrow-up {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 10px solid var(--arrow-color, #888);
|
||||
}
|
||||
|
||||
/* Phase-Anzeige am Terminal */
|
||||
.terminal-phase {
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Abgang-Label (vertikal) */
|
||||
.terminal-label {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
max-height: 90px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Add Button in Carrier */
|
||||
.terminal-label .cable-info {
|
||||
font-weight: normal;
|
||||
font-size: 8px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Output-Zeile braucht mehr Platz wenn Labels vorhanden */
|
||||
/* Add Button in Carrier (letzte Spalte, Zeile 2) */
|
||||
.btn-add-equipment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: 2px dashed var(--colorborder);
|
||||
|
|
@ -972,10 +1000,11 @@ body {
|
|||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: none;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: var(--safe-bottom);
|
||||
padding: 16px;
|
||||
padding-bottom: calc(16px + var(--safe-bottom));
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
|
|
@ -988,23 +1017,25 @@ body {
|
|||
max-height: 85vh;
|
||||
background: var(--colorbackline);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.3s ease;
|
||||
animation: modalFadeIn 0.25s ease;
|
||||
}
|
||||
|
||||
.modal-small {
|
||||
max-height: auto;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1051,21 +1082,101 @@ body {
|
|||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--colorborder);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.modal-footer-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer-right .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Wenn kein Delete-Button sichtbar: Footer-Right nimmt volle Breite */
|
||||
.modal-footer .btn-danger.hidden + .modal-footer-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPE GRID
|
||||
============================================ */
|
||||
|
||||
.step-values-area {
|
||||
display: none;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--colorborder);
|
||||
/* Zurück-Button im Modal-Header */
|
||||
.btn-back-modal {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--colortext);
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-back-modal svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Dynamische Felder */
|
||||
#eq-dynamic-fields .form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
#eq-dynamic-fields .form-select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
color: var(--colortext);
|
||||
font-size: 16px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 24px;
|
||||
}
|
||||
|
||||
#eq-dynamic-fields .form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
color: var(--colortext);
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field-required {
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
border-color: var(--danger) !important;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
|
|
@ -1100,6 +1211,12 @@ body {
|
|||
background: rgba(173, 140, 79, 0.15);
|
||||
}
|
||||
|
||||
.type-btn.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.type-btn-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 6px;
|
||||
|
|
@ -1135,31 +1252,124 @@ body {
|
|||
background: rgba(173, 140, 79, 0.15);
|
||||
}
|
||||
|
||||
/* Value Quick Select */
|
||||
.value-quick {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
/* Connection Modal - Phase Grid */
|
||||
.phase-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.value-chip {
|
||||
padding: 10px 16px;
|
||||
.phase-btn {
|
||||
padding: 10px 4px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
color: var(--colortext);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-chip:active,
|
||||
.value-chip.selected {
|
||||
background: var(--butactionbg);
|
||||
.phase-btn:active,
|
||||
.phase-btn.selected {
|
||||
border-color: var(--butactionbg);
|
||||
color: var(--textbutaction);
|
||||
background: rgba(173, 140, 79, 0.15);
|
||||
}
|
||||
|
||||
/* Phasen-Farben für Buttons */
|
||||
.phase-btn[data-type="L1"].selected { border-color: #8B4513; background: rgba(139,69,19,0.2); }
|
||||
.phase-btn[data-type="L2"].selected { border-color: #555; background: rgba(50,50,50,0.3); }
|
||||
.phase-btn[data-type="L3"].selected { border-color: #888; background: rgba(100,100,100,0.2); }
|
||||
.phase-btn[data-type="N"].selected { border-color: #0066cc; background: rgba(0,102,204,0.2); }
|
||||
.phase-btn[data-type="PE"].selected { border-color: #27ae60; background: rgba(39,174,96,0.2); }
|
||||
.phase-btn[data-type="L1N"].selected { border-color: #8B4513; background: rgba(139,69,19,0.2); }
|
||||
.phase-btn[data-type="3P"].selected { border-color: #e74c3c; background: rgba(231,76,60,0.2); }
|
||||
.phase-btn[data-type="3P+N"].selected { border-color: #e74c3c; background: rgba(231,76,60,0.2); }
|
||||
.phase-btn[data-type="L2N"].selected { border-color: #555; background: rgba(50,50,50,0.3); }
|
||||
.phase-btn[data-type="L3N"].selected { border-color: #888; background: rgba(100,100,100,0.2); }
|
||||
.phase-btn[data-type="DATA"].selected { border-color: #9b59b6; background: rgba(155,89,182,0.2); }
|
||||
|
||||
/* Abgangsseite-Buttons */
|
||||
.side-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.side-btn {
|
||||
padding: 10px 4px;
|
||||
background: var(--colorbackinput);
|
||||
border: 2px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
color: var(--colortext);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.side-btn:active,
|
||||
.side-btn.selected {
|
||||
border-color: var(--butactionbg);
|
||||
background: rgba(173, 140, 79, 0.15);
|
||||
}
|
||||
|
||||
/* Connection Modal - Typ + Farbe nebeneinander */
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-group-grow {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-group-color {
|
||||
flex: 0 0 auto;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.form-color {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 2px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Connection Modal - optgroup Styles */
|
||||
#modal-connection .form-select optgroup {
|
||||
font-weight: bold;
|
||||
color: var(--colortext);
|
||||
}
|
||||
|
||||
/* Connection Modal - form-select/form-input global */
|
||||
#modal-connection .form-select,
|
||||
#modal-connection .form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--colorbackinput);
|
||||
border: 1px solid var(--colorborder);
|
||||
border-radius: 8px;
|
||||
color: var(--colortext);
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#modal-connection .form-select {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 24px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
|
|||
|
|
@ -7406,27 +7406,45 @@
|
|||
|
||||
// Check if carrier has position data (use typeof to allow 0 values)
|
||||
if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') {
|
||||
// Find last equipment position
|
||||
var lastPos = 0;
|
||||
// Belegte Slots ermitteln (1-basiert)
|
||||
var totalTE = parseInt(carrier.total_te) || 12;
|
||||
var occupied = {};
|
||||
var lastEquipment = null;
|
||||
var lastEndPos = 0;
|
||||
carrierEquipment.forEach(function(eq) {
|
||||
var endPos = (parseInt(eq.position_te) || 1) + (parseInt(eq.width_te) || 1) - 1;
|
||||
if (endPos > lastPos) {
|
||||
lastPos = endPos;
|
||||
var pos = parseInt(eq.position_te) || 1;
|
||||
var w = parseInt(eq.width_te) || 1;
|
||||
var endPos = pos + w - 1;
|
||||
for (var s = pos; s <= endPos; s++) {
|
||||
occupied[s] = true;
|
||||
}
|
||||
if (endPos > lastEndPos) {
|
||||
lastEndPos = endPos;
|
||||
lastEquipment = eq;
|
||||
}
|
||||
});
|
||||
|
||||
// Maximale zusammenhängende Lücke berechnen
|
||||
var maxGap = 0;
|
||||
var currentGap = 0;
|
||||
for (var s = 1; s <= totalTE; s++) {
|
||||
if (!occupied[s]) {
|
||||
currentGap++;
|
||||
if (currentGap > maxGap) maxGap = currentGap;
|
||||
} else {
|
||||
currentGap = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Carrier-Objekt merkt sich maximale Lücke für Typ-Filter
|
||||
carrier._maxGap = maxGap;
|
||||
|
||||
// Calculate button positions - place buttons on the LEFT side of the carrier
|
||||
// Rail label is at about carrier._x - 44, so buttons need to be further left
|
||||
var btnX = carrier._x - 100; // Left of the carrier and rail label
|
||||
var btnY = carrier._y - self.BLOCK_HEIGHT / 2 + 10; // Aligned with blocks
|
||||
|
||||
// Check if there's space left
|
||||
var totalTE = parseInt(carrier.total_te) || 12;
|
||||
console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): lastPos=' + lastPos + ', totalTE=' + totalTE + ', hasSpace=' + (lastPos < totalTE) + ', lastEquipment=' + (lastEquipment ? lastEquipment.id : 'none'));
|
||||
|
||||
if (lastPos < totalTE) {
|
||||
if (maxGap > 0) {
|
||||
// Add Equipment button - positioned left of carrier
|
||||
var $addEquipment = $('<button class="schematic-add-equipment" data-carrier-id="' + carrier.id + '" title="Equipment hinzufügen" style="position:absolute;left:' + btnX + 'px;top:' + btnY + 'px;' + btnStyle + '">+</button>');
|
||||
$controls.append($addEquipment);
|
||||
|
|
@ -7498,7 +7516,10 @@
|
|||
$(document).off('click.addEquipment').on('click.addEquipment', '.schematic-add-equipment', function(e) {
|
||||
e.preventDefault();
|
||||
var carrierId = $(this).data('carrier-id');
|
||||
self.showAddEquipmentDialog(carrierId);
|
||||
// Maximale Lücke vom Carrier holen für Typ-Filter
|
||||
var carrier = self.carriers.find(function(c) { return String(c.id) === String(carrierId); });
|
||||
var maxGap = carrier ? (carrier._maxGap || 99) : 99;
|
||||
self.showAddEquipmentDialog(carrierId, maxGap);
|
||||
});
|
||||
|
||||
// Copy Equipment (single copy)
|
||||
|
|
@ -8010,8 +8031,9 @@
|
|||
});
|
||||
},
|
||||
|
||||
showAddEquipmentDialog: function(carrierId) {
|
||||
showAddEquipmentDialog: function(carrierId, maxGap) {
|
||||
var self = this;
|
||||
maxGap = maxGap || 99;
|
||||
|
||||
// Load equipment types first
|
||||
$.ajax({
|
||||
|
|
@ -8020,13 +8042,13 @@
|
|||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.showEquipmentTypeSelector(carrierId, response.types);
|
||||
self.showEquipmentTypeSelector(carrierId, response.types, maxGap);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showEquipmentTypeSelector: function(carrierId, types) {
|
||||
showEquipmentTypeSelector: function(carrierId, types, maxGap) {
|
||||
var self = this;
|
||||
|
||||
// Kategorisiere Equipment-Typen
|
||||
|
|
@ -8037,8 +8059,12 @@
|
|||
'klemme': { label: 'Klemmen', icon: 'fa-th-list', items: [] }
|
||||
};
|
||||
|
||||
// Sortiere Typen in Kategorien
|
||||
// Sortiere Typen in Kategorien (nur wenn Breite in verfügbare Lücke passt)
|
||||
maxGap = maxGap || 99;
|
||||
types.forEach(function(t) {
|
||||
var typeWidth = parseInt(t.width_te) || 1;
|
||||
if (typeWidth > maxGap) return; // Passt nicht in verfügbare Lücke
|
||||
|
||||
var ref = (t.ref || '').toUpperCase();
|
||||
var label = (t.label || '').toLowerCase();
|
||||
|
||||
|
|
|
|||
149
pwa.php
149
pwa.php
|
|
@ -44,7 +44,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
|||
<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">
|
||||
<link rel="stylesheet" href="css/pwa.css?v=2.7">
|
||||
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -125,9 +125,6 @@ $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>
|
||||
</button>
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -146,42 +143,130 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automat hinzufügen Modal -->
|
||||
<!-- Automat hinzufügen/bearbeiten Modal -->
|
||||
<div id="modal-add-equipment" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>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 -->
|
||||
<!-- 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>
|
||||
|
||||
<!-- Werte + Abgang (erscheint nach Typ-Auswahl) -->
|
||||
<div id="step-values" class="step-values-area">
|
||||
<div id="value-fields" class="value-fields">
|
||||
<!-- Dynamische Quick-Select Chips -->
|
||||
<!-- 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>Abgang / Beschriftung</label>
|
||||
<input type="text" id="equipment-label" placeholder="z.B. Küche Licht, Steckdose Bad">
|
||||
<label>Bezeichnung</label>
|
||||
<input type="text" id="equipment-label" placeholder="Leer = automatisch (z.B. R1.3)">
|
||||
</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 class="modal-footer">
|
||||
<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>
|
||||
|
||||
<!-- Hutschiene hinzufügen Modal -->
|
||||
<!-- 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-output-fields">
|
||||
<div class="form-group">
|
||||
<label>Abgangsseite</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>
|
||||
<div class="form-group">
|
||||
<label>Medium</label>
|
||||
<input type="text" id="conn-medium-type" class="form-input" placeholder="z.B. NYM-J, CAT6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Spezifikation</label>
|
||||
<input type="text" id="conn-medium-spec" class="form-input" placeholder="z.B. 3x1,5mm²">
|
||||
</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>Hutschiene</h2>
|
||||
<h2 id="carrier-modal-title">Hutschiene</h2>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
|
@ -198,8 +283,10 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary modal-close">Abbrechen</button>
|
||||
<button id="btn-save-carrier" class="btn btn-primary">Hinzufügen</button>
|
||||
<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>
|
||||
|
|
@ -218,8 +305,10 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary modal-close">Abbrechen</button>
|
||||
<button id="btn-save-panel" class="btn btn-primary">Hinzufügen</button>
|
||||
<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>
|
||||
|
|
@ -235,6 +324,6 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
|||
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
|
||||
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
|
||||
</script>
|
||||
<script src="js/pwa.js"></script>
|
||||
<script src="js/pwa.js?v=2.7"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
29
sw.js
29
sw.js
|
|
@ -3,8 +3,8 @@
|
|||
* Offline-First für Schaltschrank-Dokumentation
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v1.8';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v1.8';
|
||||
const CACHE_NAME = 'kundenkarte-pwa-v2.7';
|
||||
const OFFLINE_CACHE = 'kundenkarte-offline-v2.7';
|
||||
|
||||
// Statische Assets die immer gecached werden
|
||||
const STATIC_ASSETS = [
|
||||
|
|
@ -106,20 +106,19 @@ self.addEventListener('fetch', event => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Statische Assets - Cache First
|
||||
// Statische Assets (JS, CSS, Bilder) - Stale-While-Revalidate
|
||||
// Liefert sofort aus Cache, holt parallel frische Version für nächsten Aufruf
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(cached => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
cache.put(event.request, clone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
return cache.match(event.request).then(cached => {
|
||||
const fetchPromise = fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}).catch(() => cached);
|
||||
|
||||
return cached || fetchPromise;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue