feat(pwa): Equipment-Detail mit Feldlabels, Typ-Kategorien

- API: field_meta mit Labels/Typ/Optionen pro Equipment-Typ hinzugefügt
- Detail-Sheet zeigt jetzt Feld-Labels statt DB-Codes (z.B. "Nennstrom (A)" statt "ampere")
- Felder in konfigurierter Reihenfolge (position) angezeigt
- Typ-Auswahl nach Kategorien gruppiert (Leitungsschutz, Schutzgeräte, etc.)
- Alle Systeme laden statt nur Elektro (fetchAllBySystem(0))
- fieldMeta wird im Offline-Cache mitgespeichert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-26 14:31:43 +01:00
parent 5b4fd16b32
commit 241229659b
3 changed files with 103 additions and 18 deletions

View file

@ -279,9 +279,9 @@ switch ($action) {
}
}
// Equipment-Typen laden (benötigt für Terminal-Position-Auflösung)
// Equipment-Typen laden (benötigt für Terminal-Position-Auflösung + Typ-Auswahl)
$eqType = new EquipmentType($db);
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
$types = $eqType->fetchAllBySystem(0, 1); // Alle Systeme, nur aktive
// Abgänge laden (Connections mit fk_target IS NULL = Ausgänge)
$outputsData = array();
@ -383,10 +383,39 @@ switch ($action) {
'label' => $t->label,
'label_short' => $t->label_short,
'width_te' => $t->width_te,
'color' => $t->color
'color' => $t->color,
'category' => $t->category
);
}
// Feld-Metadaten pro Typ laden (Labels, Typ, Optionen)
$fieldMetaData = array();
$usedTypeIds = array_unique(array_map(function($e) { return (int) $e['fk_equipment_type']; }, $equipmentData));
if (!empty($usedTypeIds)) {
$sql = "SELECT fk_equipment_type, field_code, field_label, field_type, field_options, show_on_block";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
$sql .= " WHERE fk_equipment_type IN (".implode(',', $usedTypeIds).")";
$sql .= " AND active = 1";
$sql .= " ORDER BY position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$typeId = (int) $obj->fk_equipment_type;
if (!isset($fieldMetaData[$typeId])) {
$fieldMetaData[$typeId] = array();
}
$fieldMetaData[$typeId][] = array(
'code' => $obj->field_code,
'label' => $obj->field_label,
'type' => $obj->field_type,
'options' => $obj->field_options,
'show_on_block' => (int) $obj->show_on_block
);
}
$db->free($resql);
}
}
$response['success'] = true;
$response['panels'] = $panelsData;
$response['carriers'] = $carriersData;
@ -394,6 +423,7 @@ switch ($action) {
$response['outputs'] = $outputsData;
$response['inputs'] = $inputsData;
$response['types'] = $typesData;
$response['field_meta'] = $fieldMetaData;
break;
// ============================================

View file

@ -1277,6 +1277,22 @@ body {
text-align: center;
}
.type-grid-category {
grid-column: 1 / -1;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--colortextlink);
padding: 8px 0 2px;
border-bottom: 1px solid var(--colorborder);
margin-bottom: -2px;
}
.type-grid-category:first-child {
padding-top: 0;
}
.te-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);

View file

@ -29,6 +29,7 @@
equipmentTypes: [],
outputs: [],
inputs: [],
fieldMeta: {},
// Offline queue
offlineQueue: [],
@ -653,6 +654,7 @@
App.equipmentTypes = response.types || [];
App.outputs = response.outputs || [];
App.inputs = response.inputs || [];
App.fieldMeta = response.field_meta || {};
// Cache for offline
localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({
@ -661,7 +663,8 @@
equipment: App.equipment,
types: App.equipmentTypes,
outputs: App.outputs,
inputs: App.inputs
inputs: App.inputs,
fieldMeta: App.fieldMeta
}));
renderEditor();
@ -677,6 +680,7 @@
App.equipmentTypes = data.types || [];
App.outputs = data.outputs || [];
App.inputs = data.inputs || [];
App.fieldMeta = data.fieldMeta || {};
renderEditor();
showToast('Offline - Zeige gecachte Daten', 'warning');
} else {
@ -830,14 +834,34 @@
}
function renderTypeGrid() {
let html = '';
const categoryLabels = {
'automat': 'Leitungsschutz',
'schutz': 'Schutzgeräte',
'steuerung': 'Steuerung & Sonstiges',
'klemme': 'Klemmen'
};
const categoryOrder = ['automat', 'schutz', 'steuerung', 'klemme'];
// Typen nach Kategorie gruppieren
const groups = {};
App.equipmentTypes.forEach(type => {
html += `
<button class="type-btn" data-type-id="${type.id}" data-width="${type.width_te || 1}">
<div class="type-btn-icon" style="color:${type.color || '#3498db'}"></div>
<div class="type-btn-label">${escapeHtml(type.label_short || type.ref || type.label)}</div>
</button>
`;
const cat = type.category || 'steuerung';
if (!groups[cat]) groups[cat] = [];
groups[cat].push(type);
});
let html = '';
categoryOrder.forEach(cat => {
if (!groups[cat] || !groups[cat].length) return;
html += `<div class="type-grid-category">${escapeHtml(categoryLabels[cat] || cat)}</div>`;
groups[cat].forEach(type => {
html += `
<button class="type-btn" data-type-id="${type.id}" data-width="${type.width_te || 1}">
<div class="type-btn-icon" style="color:${type.color || '#3498db'}"></div>
<div class="type-btn-label">${escapeHtml(type.label_short || type.ref || type.label)}</div>
</button>
`;
});
});
$('#type-grid').html(html);
}
@ -1461,17 +1485,32 @@
// Body zusammenbauen
let html = '';
// Feldwerte
// Feldwerte mit Labels aus Feld-Metadaten
if (eq.field_values && Object.keys(eq.field_values).length) {
const typeMeta = App.fieldMeta ? App.fieldMeta[eq.fk_equipment_type] : null;
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>`;
if (typeMeta && typeMeta.length) {
// Felder in der konfigurierten Reihenfolge anzeigen
typeMeta.forEach(function(fm) {
const val = eq.field_values[fm.code];
if (val === '' || val === null || val === undefined) return;
html += `<div class="detail-field-row">
<span class="detail-field-label">${escapeHtml(fm.label)}</span>
<span class="detail-field-value">${escapeHtml(String(val))}</span>
</div>`;
});
} else {
// Fallback: Code als Label
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>';
}