feat(pwa): Equipment-Detail Bottom-Sheet

Tipp auf Equipment-Block zeigt jetzt Detail-Ansicht statt direkt
den Bearbeiten-Dialog zu öffnen. Bottom-Sheet mit:
- Typ-Badge + Bezeichnung
- Alle Feldwerte
- Abgänge mit Phasenfarben und Medium-Info
- Einspeisungen
- Position (Hutschiene + TE)
- "Bearbeiten"-Button öffnet Edit-Dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-26 12:28:30 +01:00
parent 4fb9d8e472
commit dcd00fe844
4 changed files with 352 additions and 11 deletions

View file

@ -1491,6 +1491,209 @@ body {
color: #000;
}
/* ============================================
EQUIPMENT DETAIL BOTTOM-SHEET
============================================ */
.bottom-sheet {
position: fixed;
inset: 0;
z-index: 1100;
display: none;
}
.bottom-sheet.active {
display: block;
}
.sheet-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.2s ease;
}
.sheet-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
max-height: 80vh;
background: var(--colorbackbody);
border-top-left-radius: 16px;
border-top-right-radius: 16px;
overflow-y: auto;
animation: slideUp 0.25s ease;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.sheet-handle {
width: 40px;
height: 4px;
background: var(--colorborder);
border-radius: 2px;
margin: 10px auto 0;
}
.sheet-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid var(--colorborder);
}
.detail-type-badge {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
color: #fff;
flex-shrink: 0;
}
.sheet-header-text {
flex: 1;
min-width: 0;
}
.sheet-header-text h2 {
font-size: 16px;
font-weight: 700;
margin: 0;
word-break: break-word;
}
.detail-subtitle {
font-size: 12px;
color: var(--colortextmuted);
margin: 2px 0 0;
}
.sheet-body {
padding: 16px;
}
.detail-section {
margin-bottom: 16px;
}
.detail-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--colortextmuted);
margin-bottom: 8px;
}
.detail-field-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.detail-field-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 6px 10px;
background: var(--colorbackinput);
border-radius: 6px;
}
.detail-field-label {
font-size: 13px;
color: var(--colortextmuted);
flex-shrink: 0;
margin-right: 12px;
}
.detail-field-value {
font-size: 13px;
font-weight: 600;
text-align: right;
word-break: break-word;
}
.detail-field-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: #fff;
}
/* Verbindungsliste */
.detail-conn-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.detail-conn-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--colorbackinput);
border-radius: 6px;
}
.detail-conn-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.detail-conn-info {
flex: 1;
min-width: 0;
}
.detail-conn-label {
font-size: 13px;
font-weight: 600;
}
.detail-conn-meta {
font-size: 11px;
color: var(--colortextmuted);
margin-top: 1px;
}
.detail-conn-arrow {
font-size: 14px;
color: var(--colortextmuted);
flex-shrink: 0;
}
.sheet-footer {
display: flex;
gap: 10px;
padding: 16px;
border-top: 1px solid var(--colorborder);
}
.sheet-footer .btn {
flex: 1;
}
/* ============================================
UTILITIES
============================================ */

130
js/pwa.js
View file

@ -194,6 +194,11 @@
$('#btn-cancel-equipment').on('click', () => closeModal('add-equipment'));
$('#btn-delete-equipment').on('click', handleDeleteEquipmentConfirm);
// Equipment Detail Bottom-Sheet
$('#btn-detail-edit').on('click', openEditFromDetail);
$('#btn-detail-close').on('click', () => $('#sheet-equipment-detail').removeClass('active'));
$('#sheet-equipment-detail .sheet-overlay').on('click', () => $('#sheet-equipment-detail').removeClass('active'));
// Terminal/Connection - Klick auf einzelne Klemme
$('#editor-content').on('click', '.terminal-point', handleTerminalClick);
$('#btn-save-connection').on('click', handleSaveConnection);
@ -1432,30 +1437,141 @@
App.editEquipmentId = null;
}
async function handleEquipmentClick() {
function handleEquipmentClick() {
const eqId = $(this).data('equipment-id');
const eq = App.equipment.find(e => e.id == eqId);
if (!eq) return;
showEquipmentDetail(eq);
}
/**
* Equipment-Detail Bottom-Sheet anzeigen
*/
function showEquipmentDetail(eq) {
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
const typeLabel = type?.label || type?.ref || 'Equipment';
const typeLabelShort = type?.label_short || type?.ref || '?';
const typeColor = eq.block_color || type?.color || '#3498db';
// Header
$('#detail-type-badge').css('background', typeColor).text(typeLabelShort);
$('#detail-title').text(eq.label || 'Automat ' + eq.id);
$('#detail-type-name').text(typeLabel);
// Body zusammenbauen
let html = '';
// Feldwerte
if (eq.field_values && Object.keys(eq.field_values).length) {
html += '<div class="detail-section">';
html += '<div class="detail-section-title">Werte</div>';
html += '<div class="detail-field-list">';
for (const [key, val] of Object.entries(eq.field_values)) {
if (val === '' || val === null || val === undefined) continue;
html += `<div class="detail-field-row">
<span class="detail-field-label">${escapeHtml(key)}</span>
<span class="detail-field-value">${escapeHtml(String(val))}</span>
</div>`;
}
html += '</div></div>';
}
// Abgänge (Outputs)
const outputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id) : [];
if (outputs.length) {
html += '<div class="detail-section">';
html += '<div class="detail-section-title">Abgänge</div>';
html += '<div class="detail-conn-list">';
outputs.forEach(o => {
const color = o.color || getPhaseColor(o.connection_type);
const label = o.output_label || o.connection_type || 'Abgang';
const meta = [o.medium_type, o.medium_spec, o.medium_length].filter(Boolean).join(' · ');
const arrow = o.is_top ? '&#9650;' : '&#9660;';
html += `<div class="detail-conn-item">
<span class="detail-conn-dot" style="background:${color}"></span>
<div class="detail-conn-info">
<div class="detail-conn-label">${escapeHtml(label)}</div>
${meta ? '<div class="detail-conn-meta">' + escapeHtml(meta) + '</div>' : ''}
</div>
<span class="detail-conn-arrow">${arrow}</span>
</div>`;
});
html += '</div></div>';
}
// Einspeisungen (Inputs)
const inputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id) : [];
if (inputs.length) {
html += '<div class="detail-section">';
html += '<div class="detail-section-title">Einspeisungen</div>';
html += '<div class="detail-conn-list">';
inputs.forEach(i => {
const color = i.color || getPhaseColor(i.connection_type);
const label = i.output_label || i.connection_type || 'Einspeisung';
html += `<div class="detail-conn-item">
<span class="detail-conn-dot" style="background:${color}"></span>
<div class="detail-conn-info">
<div class="detail-conn-label">${escapeHtml(label)}</div>
</div>
<span class="detail-conn-arrow">&#9660;</span>
</div>`;
});
html += '</div></div>';
}
// Position-Info
const carrier = App.carriers.find(c => c.id == eq.fk_carrier);
html += '<div class="detail-section">';
html += '<div class="detail-section-title">Position</div>';
html += '<div class="detail-field-list">';
if (carrier) {
html += `<div class="detail-field-row">
<span class="detail-field-label">Hutschiene</span>
<span class="detail-field-value">${escapeHtml(carrier.label || 'Hutschiene')}</span>
</div>`;
}
html += `<div class="detail-field-row">
<span class="detail-field-label">TE-Position</span>
<span class="detail-field-value">${eq.position_te || ''} (${eq.width_te || 1} TE breit)</span>
</div>`;
html += '</div></div>';
if (!html) {
html = '<p class="text-muted text-center">Keine Details vorhanden</p>';
}
$('#detail-body').html(html);
// Bearbeiten-Button: Equipment-ID merken
App.detailEquipmentId = eq.id;
$('#sheet-equipment-detail').addClass('active');
}
/**
* Detail-Sheet schließen und Edit-Modal öffnen
*/
async function openEditFromDetail() {
const eqId = App.detailEquipmentId;
$('#sheet-equipment-detail').removeClass('active');
const eq = App.equipment.find(e => e.id == eqId);
if (!eq) return;
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
// Edit-Modus setzen
App.editEquipmentId = eqId;
App.currentCarrierId = eq.fk_carrier;
App.selectedTypeId = eq.fk_equipment_type;
// Titel + Buttons anpassen
$('#eq-fields-title').text(type?.label_short || type?.label || 'Bearbeiten');
$('#btn-save-equipment').text('Aktualisieren');
$('#btn-delete-equipment').removeClass('hidden');
// Label befüllen
$('#equipment-label').val(eq.label || '');
// Felder vom Server laden (mit bestehenden Werten)
await loadTypeFields(eq.fk_equipment_type, eqId);
// Direkt Schritt 2 zeigen (kein Typ-Wechsel beim Edit)
showEquipmentStep('fields');
openModal('add-equipment');
}

26
pwa.php
View file

@ -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?v=2.8">
<link rel="stylesheet" href="css/pwa.css?v=2.9">
<style>:root { --primary: <?php echo $themeColor; ?>; }</style>
</head>
<body>
@ -313,6 +313,28 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
</div>
</div>
<!-- Equipment Detail Bottom-Sheet -->
<div id="sheet-equipment-detail" class="bottom-sheet">
<div class="sheet-overlay"></div>
<div class="sheet-content">
<div class="sheet-handle"></div>
<div class="sheet-header">
<div id="detail-type-badge" class="detail-type-badge"></div>
<div class="sheet-header-text">
<h2 id="detail-title">Equipment</h2>
<p id="detail-type-name" class="detail-subtitle"></p>
</div>
</div>
<div id="detail-body" class="sheet-body">
<!-- Dynamischer Inhalt -->
</div>
<div class="sheet-footer">
<button id="btn-detail-edit" class="btn btn-primary">Bearbeiten</button>
<button id="btn-detail-close" class="btn btn-secondary">Schließen</button>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toast" class="toast"></div>
@ -324,6 +346,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?v=2.8"></script>
<script src="js/pwa.js?v=2.9"></script>
</body>
</html>

4
sw.js
View file

@ -3,8 +3,8 @@
* Offline-First für Schaltschrank-Dokumentation
*/
const CACHE_NAME = 'kundenkarte-pwa-v2.8';
const OFFLINE_CACHE = 'kundenkarte-offline-v2.8';
const CACHE_NAME = 'kundenkarte-pwa-v2.9';
const OFFLINE_CACHE = 'kundenkarte-offline-v2.9';
// Statische Assets die immer gecached werden
const STATIC_ASSETS = [