Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', ['page_title' => $page_title, 'current_admin_page' => $current_admin_page ?? 'netdiagram-devices']); ?>
<div class="settings-page">
<!-- ── Neue Zeile anlegen ──────────────────────────────────────── -->
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">🔁</span>
<div>
<h2><?= $this->te('Neuer Gerätetyp') ?></h2>
</div>
</div>
<div class="settings-card-body">
<button type="button" class="btn btn-primary" onclick="ndAdminOpenCreate()"><?= $this->te('Gerätetyp anlegen') ?></button>
</div>
</section>
<!-- ── Tabelle ─────────────────────────────────────────────────── -->
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">⚙</span>
<div>
<h2><?= $this->te('Gerätetypen') ?></h2>
<p class="settings-card-desc"><?= $this->te('{anzahl} Typ(en) vorhanden · Reihenfolge durch Ziehen ändern', ['anzahl' => count($types)]) ?></p>
</div>
</div>
<div class="settings-card-body" style="padding:0;">
<?php if (empty($types)): ?>
<p style="padding:1.25rem;color:var(--gray-500);font-size:.85rem;"><?= $this->te('Noch keine Gerätetypen definiert.') ?></p>
<?php else: ?>
<div class="table-wrapper" style="border:none;border-radius:0;">
<table class="data-table" id="ndTypeTableBody">
<thead>
<tr>
<th scope="col" style="width:28px;"></th>
<th scope="col"><?= $this->te('Name') ?></th>
<th scope="col"><?= $this->te('Schlüssel') ?></th>
<th scope="col" style="width:80px;"><?= $this->te('Status') ?></th>
<th scope="col" style="width:110px;text-align:right;"><?= $this->te('Aktionen') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($types as $t): ?>
<tr data-id="<?= $t['id'] ?>" id="nd-type-row-<?= $t['id'] ?>" class="<?= $t['is_active'] ? '' : 'nd-inactive' ?>">
<td style="cursor:grab;color:var(--gray-300);text-align:center;padding:.55rem .4rem;">
<svg width="10" height="14" viewBox="0 0 10 16" fill="currentColor"><circle cx="3" cy="2" r="1.3"/><circle cx="7" cy="2" r="1.3"/><circle cx="3" cy="8" r="1.3"/><circle cx="7" cy="8" r="1.3"/><circle cx="3" cy="14" r="1.3"/><circle cx="7" cy="14" r="1.3"/></svg>
</td>
<td>
<div style="display:flex;align-items:center;gap:.6rem;">
<div style="width:30px;height:30px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;background:<?= htmlspecialchars($t['bg_color']) ?>;border:1.5px solid <?= htmlspecialchars($t['stroke_color']) ?>;">
<?php if ($t['icon_type'] === 'image'): ?>
<img alt="" src="<?= htmlspecialchars($t['icon_value']) ?>" style="width:18px;height:18px;object-fit:contain;">
<?php else: ?>
<?= htmlspecialchars($t['icon_value']) ?>
<?php endif; ?>
</div>
<strong><?= htmlspecialchars($t['label']) ?></strong>
</div>
</td>
<td><code style="font-size:.78rem;background:var(--gray-100);padding:2px 6px;border-radius:3px;color:var(--gray-700);"><?= htmlspecialchars($t['type_key']) ?></code></td>
<td>
<button onclick="ndToggleActive(<?= $t['id'] ?>, this)"
class="nd-status-btn <?= $t['is_active'] ? 'nd-status-active' : 'nd-status-inactive' ?>"
title="<?= $this->te('Klicken zum Umschalten') ?>">
<?= $t['is_active'] ? 'Aktiv' : 'Inaktiv' ?>
</button>
</td>
<td style="text-align:right;">
<button class="btn btn-xs btn-default"
onclick="ndAdminOpenEdit(<?= htmlspecialchars(json_encode($t)) ?>)"><?= $this->te('Bearbeiten') ?></button>
<form method="POST" action="/admin/netdiagram/devices/<?= $t['id'] ?>/delete"
style="display:inline;"
onsubmit="return confirm('Gerätetyp "<?= htmlspecialchars(addslashes($t['label'])) ?>" wirklich löschen?')">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-xs btn-danger"><?= $this->te('Löschen') ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div style="padding:.75rem 1.25rem;border-top:1px solid var(--gray-100);background:var(--gray-50);font-size:.78rem;color:var(--gray-500);">
<?= $this->t('Der Schlüssel (z.B. <code>switch</code>) wird in gespeicherten Diagrammen verwendet. Einmal angelegte Schlüssel <strong>nicht umbenennen</strong>, da sonst bestehende Diagramme betroffen sind.') ?>
</div>
</section>
<div style="padding:0 0 2rem;">
<a href="/netdiagram" class="btn btn-secondary"><?= $this->te('← Zu den Netzwerkplänen') ?></a>
</div>
</div><!-- /.settings-page -->
<!-- ══ Modal: Anlegen / Bearbeiten ══════════════════════════════════════ -->
<div id="ndAdminModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:1000;align-items:center;justify-content:center;">
<div style="background:var(--color-bg);border-radius:.75rem;width:520px;max-width:95vw;max-height:92vh;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.2);" onclick="event.stopPropagation()">
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--gray-200);flex-shrink:0;">
<h3 id="ndAdminModalTitle" style="margin:0;font-size:1rem;font-weight:600;"><?= $this->te('Neuer Gerätetyp') ?></h3>
<button type="button" onclick="ndAdminCloseModal()" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<div style="overflow-y:auto;flex:1;">
<form id="ndAdminForm" method="POST" action="/admin/netdiagram/devices" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" id="ndFormIconType" name="icon_type" value="emoji">
<input type="hidden" id="ndFormIconValue" name="icon_value" value="">
<div style="padding:1.25rem;display:flex;flex-direction:column;gap:.9rem;">
<div style="display:flex;gap:.75rem;">
<div class="settings-field" style="flex:1;">
<label class="settings-label"><?= $this->te('Name') ?> <span class="req">*</span></label>
<input type="text" name="label" id="ndFormLabel" class="settings-input" placeholder="<?= $this->te('z.B. Core-Switch') ?>" required>
</div>
<div class="settings-field" style="width:110px;">
<label for="ndFormSort" class="settings-label"><?= $this->te('Reihenfolge') ?></label>
<input type="number" name="sort_order" id="ndFormSort" class="settings-input" value="99" min="0" max="9999">
</div>
</div>
<div class="settings-field">
<label class="settings-label">
<?= $this->te('Schlüssel') ?> <span class="req">*</span>
<span style="font-weight:400;color:var(--gray-400);"><?= $this->te('(nur a-z, 0-9, _)') ?></span>
</label>
<input type="text" name="type_key" id="ndFormKey" class="settings-input"
placeholder="<?= $this->te('z.B. core_switch') ?>" pattern="[a-z0-9_]+" required>
<small style="font-size:.72rem;color:var(--gray-400);"><?= $this->te('Nach dem Anlegen nicht mehr ändern!') ?></small>
</div>
<!-- Icon -->
<div class="settings-field">
<label class="settings-label"><?= $this->te('Icon') ?></label>
<div style="display:flex;align-items:center;gap:.75rem;">
<div id="ndIconPreviewBox" style="width:44px;height:44px;border-radius:8px;background:var(--gray-100);border:2px solid var(--gray-300);display:flex;align-items:center;justify-content:center;font-size:1.5rem;flex-shrink:0;">◆</div>
<div>
<div id="ndIconPreviewLabel" style="font-size:.8rem;color:var(--gray-500);margin-bottom:.35rem;"><?= $this->te('Kein Icon gewählt') ?></div>
<div style="display:flex;gap:.4rem;">
<button type="button" onclick="ndOpenIconPicker()" class="btn btn-secondary" style="font-size:.8rem;padding:.3rem .7rem;"><?= $this->te('Icon-Bibliothek') ?></button>
<label class="btn btn-secondary" style="font-size:.8rem;padding:.3rem .7rem;cursor:pointer;margin:0;">
<?= $this->te('Bild hochladen') ?>
<input type="file" name="icon_file" id="ndIconFileInput" style="display:none;"
accept="image/png,image/jpeg,image/gif,image/svg+xml,image/webp"
onchange="ndHandleIconUpload(this)">
</label>
</div>
</div>
</div>
</div>
<!-- Colors -->
<div style="display:flex;gap:.75rem;">
<div class="settings-field" style="flex:1;">
<label class="settings-label"><?= $this->te('Hintergrundfarbe') ?></label>
<div style="display:flex;align-items:center;gap:.5rem;">
<input type="color" name="bg_color" id="ndFormBg" value="#f1f5f9"
style="width:34px;height:34px;padding:2px;cursor:pointer;border-radius:4px;border:1px solid var(--gray-300);"
oninput="this.nextElementSibling.value=this.value;ndUpdateColorPreview()">
<input type="text" id="ndFormBgText" value="#f1f5f9" class="settings-input"
style="width:90px;font-family:monospace;"
oninput="document.getElementById('ndFormBg').value=this.value;ndUpdateColorPreview()">
</div>
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-top:.4rem;">
<?php foreach (['#dbeafe','#ede9fe','#fee2e2','#d1fae5','#fef3c7','#f3f4f6','#cffafe','#fce7f3','#f1f5f9','#ecfdf5','#fff7ed','#f0fdf4'] as $c): ?>
<div onclick="document.getElementById('ndFormBg').value=<?= $this->e(json_encode($c, JSON_HEX_APOS | JSON_HEX_QUOT)) ?>;document.getElementById('ndFormBgText').value=<?= $this->e(json_encode($c, JSON_HEX_APOS | JSON_HEX_QUOT)) ?>;ndUpdateColorPreview();"
style="width:16px;height:16px;border-radius:3px;background:<?= $this->e($c) ?>;border:1px solid rgba(0,0,0,.1);cursor:pointer;" title="<?= $this->e($c) ?>"></div>
<?php endforeach; ?>
</div>
</div>
<div class="settings-field" style="flex:1;">
<label class="settings-label"><?= $this->te('Rahmenfarbe') ?></label>
<div style="display:flex;align-items:center;gap:.5rem;">
<input type="color" name="stroke_color" id="ndFormStroke" value="#475569"
style="width:34px;height:34px;padding:2px;cursor:pointer;border-radius:4px;border:1px solid var(--gray-300);"
oninput="this.nextElementSibling.value=this.value;ndUpdateColorPreview()">
<input type="text" id="ndFormStrokeText" value="#475569" class="settings-input"
style="width:90px;font-family:monospace;"
oninput="document.getElementById('ndFormStroke').value=this.value;ndUpdateColorPreview()">
</div>
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-top:.4rem;">
<?php foreach (['#3b82f6','#8b5cf6','#ef4444','#10b981','#f59e0b','#6b7280','#06b6d4','#ec4899','#475569','#16a34a','#ea580c','#7c3aed'] as $c): ?>
<div onclick="document.getElementById('ndFormStroke').value=<?= $this->e(json_encode($c, JSON_HEX_APOS | JSON_HEX_QUOT)) ?>;document.getElementById('ndFormStrokeText').value=<?= $this->e(json_encode($c, JSON_HEX_APOS | JSON_HEX_QUOT)) ?>;ndUpdateColorPreview();"
style="width:16px;height:16px;border-radius:3px;background:<?= $this->e($c) ?>;border:1px solid rgba(0,0,0,.1);cursor:pointer;" title="<?= $this->e($c) ?>"></div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:.5rem;padding:.85rem 1.25rem;border-top:1px solid var(--gray-200);background:var(--gray-50);flex-shrink:0;">
<button type="button" class="btn btn-secondary" onclick="ndAdminCloseModal()"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary" id="ndAdminSubmitBtn"><?= $this->te('Anlegen') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- ══ Icon-Picker Modal ══════════════════════════════════════════════ -->
<div id="ndIconPickerModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1010;align-items:center;justify-content:center;">
<div style="background:var(--color-bg);border-radius:.75rem;width:640px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.2);" onclick="event.stopPropagation()">
<div style="display:flex;align-items:center;justify-content:space-between;padding:.85rem 1.25rem;border-bottom:1px solid var(--gray-200);flex-shrink:0;">
<h3 style="margin:0;font-size:1rem;font-weight:600;"><?= $this->te('Icon auswählen') ?></h3>
<button type="button" onclick="ndCloseIconPicker()" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<div style="padding:.6rem .75rem;border-bottom:1px solid var(--gray-100);flex-shrink:0;">
<input type="text" id="ndIconSearch" class="settings-input" placeholder="<?= $this->te('Icons suchen…') ?>" oninput="ndFilterIcons(this.value)">
</div>
<div style="padding:.4rem .75rem;border-bottom:1px solid var(--gray-100);flex-shrink:0;display:flex;gap:.3rem;flex-wrap:wrap;" id="ndIconCategoryBtns"></div>
<div style="overflow-y:auto;flex:1;padding:.5rem .75rem;">
<div id="ndIconGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(60px,1fr));gap:4px;"></div>
</div>
<div style="padding:.65rem 1.25rem;border-top:1px solid var(--gray-200);background:var(--gray-50);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;">
<span id="ndIconPickerSelected" style="font-size:.82rem;color:var(--gray-500);"><?= $this->te('Kein Icon ausgewählt') ?></span>
<button onclick="ndConfirmIconPicker()" class="btn btn-primary" style="font-size:.82rem;" id="ndIconPickerConfirmBtn" disabled><?= $this->te('Übernehmen') ?></button>
</div>
</div>
</div>
<style>
.nd-inactive td { opacity:.45; }
.nd-status-btn {
padding:.2rem .55rem; font-size:.75rem; border-radius:.375rem;
border:1px solid; cursor:pointer; font-weight:500;
}
.nd-status-active { background:var(--emerald-100); border-color:var(--emerald-300); color:var(--emerald-800); }
.nd-status-inactive { background:var(--gray-100); border-color:var(--gray-300); color:var(--gray-500); }
.nd-status-active:hover { background:var(--emerald-200); }
.nd-status-inactive:hover { background:var(--gray-200); }
.nd-icon-btn {
display:flex; flex-direction:column; align-items:center; gap:2px;
padding:.4rem .2rem; border-radius:6px; border:2px solid transparent;
cursor:pointer; background:var(--slate-50); transition:all .12s; min-height:56px;
justify-content:center;
}
.nd-icon-btn:hover { background:var(--blue-50); border-color:var(--blue-200); }
.nd-icon-btn.chosen { background:var(--blue-100); border-color:var(--blue-500); }
.nd-icon-glyph { font-size:1.4rem; line-height:1.1; }
.nd-icon-name { font-size:.6rem; color:var(--color-secondary); text-align:center; line-height:1.1;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:56px; }
.nd-icon-section-title {
font-size:.7rem; font-weight:700; color:var(--gray-400); text-transform:uppercase;
letter-spacing:.06em; padding:.4rem .25rem .2rem; grid-column:1/-1;
}
.nd-cat-chip {
padding:.25rem .65rem; border-radius:1rem; border:1px solid var(--gray-300);
background:var(--gray-50); font-size:.78rem; cursor:pointer; color:var(--gray-600);
}
.nd-cat-chip.active {
background:var(--color-primary); border-color:var(--color-primary); color:var(--color-on-accent);
}
</style>
<script>
// ── Icon Library ─────────────────────────────────────────────────────────────
const ND_ICONS = {
'Netzwerk': [
{g:'⇅',n:'Switch'}, {g:'↻',n:'Router'}, {g:'🛡',n:'Firewall'},
{g:'📡',n:'Access Point'},{g:'☁',n:'Cloud'}, {g:'🌐',n:'Internet'},
{g:'🔀',n:'Load Balancer'},{g:'📶',n:'WLAN'}, {g:'🔌',n:'Port'},
{g:'🔗',n:'Link'}, {g:'🌉',n:'Bridge'}, {g:'🔄',n:'Proxy'},
{g:'⚡',n:'PoE/Power'}, {g:'🔒',n:'Gesichert'}, {g:'🔓',n:'Offen'},
{g:'🚧',n:'DMZ'}, {g:'⛓',n:'Kette'}, {g:'🔁',n:'Loop'},
{g:'🗺',n:'Topologie'}, {g:'📍',n:'Standort'}, {g:'🏠',n:'Filiale'},
{g:'🏢',n:'Zentrale'}, {g:'🏭',n:'Rechenzentrum'},{g:'🗼',n:'Turm'},
{g:'📻',n:'Funk'},
],
'Server & Storage': [
{g:'🖥',n:'Server'}, {g:'💽',n:'HDD'}, {g:'💾',n:'SSD'},
{g:'📀',n:'Optisch'}, {g:'🗄',n:'Rack/NAS'}, {g:'📦',n:'Container'},
{g:'🖨',n:'Druck'}, {g:'📊',n:'Datenbank'}, {g:'📈',n:'Monitoring'},
{g:'⚙',n:'Dienst'}, {g:'🔧',n:'Konfiguration'},{g:'🧩',n:'Modul'},
{g:'📋',n:'Config'}, {g:'🗂',n:'Dateien'}, {g:'💡',n:'Logik'},
{g:'🔬',n:'Analyse'}, {g:'🏷',n:'Label'}, {g:'📁',n:'Ordner'},
{g:'🖲',n:'Verwaltung'}, {g:'🔑',n:'Schlüssel'}, {g:'🗝',n:'Auth'},
{g:'🔐',n:'Vault'}, {g:'📉',n:'Log'}, {g:'⏱',n:'Scheduler'},
{g:'🧮',n:'Compute'},
],
'Endgeräte': [
{g:'💻',n:'Laptop'}, {g:'🖥',n:'Desktop'}, {g:'📱',n:'Smartphone'},
{g:'☎',n:'Telefon'}, {g:'📞',n:'Tischtelefon'}, {g:'📲',n:'Tablet'},
{g:'📺',n:'Bildschirm'}, {g:'📷',n:'Kamera'}, {g:'🎮',n:'Gaming'},
{g:'⌚',n:'IoT Wearable'},{g:'💡',n:'Smart-Gerät'}, {g:'🔋',n:'USV/Batterie'},
{g:'📡',n:'Sensor'}, {g:'🕹',n:'Controller'}, {g:'🖱',n:'Maus'},
{g:'⌨',n:'Tastatur'}, {g:'🖨',n:'Drucker'}, {g:'📟',n:'Pager'},
{g:'📠',n:'Fax'}, {g:'🎙',n:'Mikrofon'}, {g:'🔊',n:'Lautsprecher'},
{g:'📸',n:'IP-Kamera'}, {g:'🚗',n:'Fahrzeug'}, {g:'✈',n:'Drohne'},
{g:'🤖',n:'Roboter'},
],
'Infrastruktur': [
{g:'⚡',n:'Strom/PoE'}, {g:'🔌',n:'Steckdose'}, {g:'🔋',n:'Batterie/UPS'},
{g:'🌡',n:'Sensor'}, {g:'🔥',n:'Wärme'}, {g:'❄',n:'Kühlung'},
{g:'🚨',n:'Alarm'}, {g:'⚠',n:'Warnung'}, {g:'🔔',n:'Benachrichtigung'},
{g:'📢',n:'Lautsprecher'},{g:'🌊',n:'Flow'}, {g:'🌀',n:'Vortex'},
{g:'💧',n:'Wasser'},{g:'🏗',n:'Infrastruktur'},{g:'🛠',n:'Wartung'},
{g:'⛽',n:'Energie'}, {g:'☀',n:'Solar'}, {g:'🌬',n:'Belüftung'},
{g:'🔭',n:'Monitoring'}, {g:'📡',n:'Mast'}, {g:'🌍',n:'Global'},
{g:'🛰',n:'Satellit'}, {g:'📻',n:'Funk'}, {g:'🔦',n:'Kontroll-Licht'},
{g:'💦',n:'Wasserkühlung'},
],
'Formen & Symbole': [
{g:'◆',n:'Raute'}, {g:'◇',n:'Raute leer'}, {g:'●',n:'Kreis'},
{g:'○',n:'Kreis leer'}, {g:'■',n:'Quadrat'}, {g:'□',n:'Quadrat leer'},
{g:'▲',n:'Dreieck oben'}, {g:'▽',n:'Dreieck unten'},{g:'⬡',n:'Hexagon'},
{g:'⬢',n:'Hexagon voll'}, {g:'★',n:'Stern'}, {g:'☆',n:'Stern leer'},
{g:'⊕',n:'Kreuz-Kreis'}, {g:'✚',n:'Plus'}, {g:'✖',n:'Kreuz'},
{g:'◉',n:'Zielscheibe'}, {g:'⬤',n:'Punkt'}, {g:'⊞',n:'Grid'},
{g:'⊟',n:'Minus-Box'}, {g:'⚪',n:'Leer'}, {g:'⚫',n:'Voll'},
{g:'🟦',n:'Blau'}, {g:'🟩',n:'Grün'}, {g:'🟥',n:'Rot'},
{g:'🟨',n:'Gelb'},
],
};
let ndPickerSelectedIcon = null;
let ndPickerCurrentCat = null;
// ── Form / Modal ──────────────────────────────────────────────────────────────
function ndAdminOpenCreate() {
document.getElementById('ndAdminModalTitle').textContent = 'Neuer Gerätetyp';
document.getElementById('ndAdminSubmitBtn').textContent = 'Anlegen';
document.getElementById('ndAdminForm').action = '/admin/netdiagram/devices';
document.getElementById('ndAdminForm').reset();
document.getElementById('ndFormIconType').value = 'emoji';
document.getElementById('ndFormIconValue').value = '◆';
ndSetIconPreview({type:'emoji', value:'◆', label:'◆'});
document.getElementById('ndFormBgText').value = '#f1f5f9';
document.getElementById('ndFormStrokeText').value = '#475569';
ndUpdateColorPreview();
document.getElementById('ndAdminModal').style.display = 'flex';
setTimeout(function(){ document.getElementById('ndFormLabel').focus(); }, 60);
}
function ndAdminOpenEdit(t) {
document.getElementById('ndAdminModalTitle').textContent = 'Gerätetyp bearbeiten';
document.getElementById('ndAdminSubmitBtn').textContent = 'Speichern';
document.getElementById('ndAdminForm').action = '/admin/netdiagram/devices/' + t.id + '/update';
document.getElementById('ndFormLabel').value = t.label;
document.getElementById('ndFormKey').value = t.type_key;
document.getElementById('ndFormKey').readOnly = true;
document.getElementById('ndFormKey').style.background = '#f3f4f6';
document.getElementById('ndFormSort').value = t.sort_order;
document.getElementById('ndFormBg').value = t.bg_color;
document.getElementById('ndFormBgText').value = t.bg_color;
document.getElementById('ndFormStroke').value = t.stroke_color;
document.getElementById('ndFormStrokeText').value = t.stroke_color;
document.getElementById('ndFormIconType').value = t.icon_type;
document.getElementById('ndFormIconValue').value = t.icon_value;
ndSetIconPreview(t.icon_type === 'image'
? {type:'image', value:t.icon_value, label:'Hochgeladenes Bild'}
: {type:'emoji', value:t.icon_value, label:t.icon_value});
ndUpdateColorPreview();
document.getElementById('ndAdminModal').style.display = 'flex';
}
function ndAdminCloseModal() {
document.getElementById('ndAdminModal').style.display = 'none';
document.getElementById('ndFormKey').readOnly = false;
document.getElementById('ndFormKey').style.background = '';
}
function ndUpdateColorPreview() {
var box = document.getElementById('ndIconPreviewBox');
box.style.background = document.getElementById('ndFormBg').value;
box.style.borderColor = document.getElementById('ndFormStroke').value;
}
function ndSetIconPreview(icon) {
var box = document.getElementById('ndIconPreviewBox');
var label = document.getElementById('ndIconPreviewLabel');
document.getElementById('ndFormIconType').value = icon.type;
document.getElementById('ndFormIconValue').value = icon.value;
if (icon.type === 'image') {
box.innerHTML = '<img src="'+icon.value+'" style="width:26px;height:26px;object-fit:contain;">';
label.textContent = 'Hochgeladenes Bild';
} else {
box.textContent = icon.value;
label.textContent = icon.label || icon.value;
}
ndUpdateColorPreview();
}
function ndHandleIconUpload(input) {
var file = input.files[0];
if (!file) return;
if (file.size > 512*1024) { alert('Maximale Dateigröße: 512 KB'); return; }
var reader = new FileReader();
reader.onload = function(e) {
ndSetIconPreview({type:'image', value:e.target.result, label:'Hochgeladenes Bild'});
document.getElementById('ndFormIconType').value = 'image';
document.getElementById('ndFormIconValue').value = '';
};
reader.readAsDataURL(file);
}
// ── Icon Picker ───────────────────────────────────────────────────────────────
function ndOpenIconPicker() {
ndPickerSelectedIcon = null;
ndPickerCurrentCat = null;
document.getElementById('ndIconPickerConfirmBtn').disabled = true;
document.getElementById('ndIconPickerSelected').textContent = 'Kein Icon ausgewählt';
document.getElementById('ndIconSearch').value = '';
ndRenderCategoryBtns();
ndRenderIconGrid(null, '');
document.getElementById('ndIconPickerModal').style.display = 'flex';
}
function ndCloseIconPicker() { document.getElementById('ndIconPickerModal').style.display = 'none'; }
function ndConfirmIconPicker() {
if (!ndPickerSelectedIcon) return;
ndSetIconPreview({type:'emoji', value:ndPickerSelectedIcon.g, label:ndPickerSelectedIcon.n});
ndCloseIconPicker();
}
function ndRenderCategoryBtns() {
var cats = Object.keys(ND_ICONS);
document.getElementById('ndIconCategoryBtns').innerHTML = cats.map(function(c) {
var active = c === ndPickerCurrentCat ? ' active' : '';
return '<button type="button" class="nd-cat-chip'+active+'" onclick="ndPickCat(\''+c+'\')">'+c+'</button>';
}).join('');
}
function ndPickCat(cat) {
ndPickerCurrentCat = (ndPickerCurrentCat === cat) ? null : cat;
document.getElementById('ndIconSearch').value = '';
ndRenderCategoryBtns();
ndRenderIconGrid(ndPickerCurrentCat, '');
}
function ndFilterIcons(q) {
ndPickerCurrentCat = null;
ndRenderCategoryBtns();
ndRenderIconGrid(null, q.toLowerCase().trim());
}
function ndRenderIconGrid(cat, query) {
var cats = cat ? [cat] : Object.keys(ND_ICONS);
var html = '';
cats.forEach(function(c) {
var icons = ND_ICONS[c].filter(function(ic) {
return !query || ic.n.toLowerCase().includes(query) || ic.g.includes(query);
});
if (!icons.length) return;
if (!cat) html += '<div class="nd-icon-section-title">'+c+'</div>';
icons.forEach(function(ic) {
var chosen = ndPickerSelectedIcon && ndPickerSelectedIcon.g === ic.g ? ' chosen' : '';
html += '<button type="button" class="nd-icon-btn'+chosen+'" onclick="ndSelectIcon(this,\''+
ic.g.replace(/'/g,"\\'")+'\',\''+ic.n.replace(/'/g,"\\'")+'\')">'+
'<span class="nd-icon-glyph">'+ic.g+'</span>'+
'<span class="nd-icon-name">'+ic.n+'</span></button>';
});
});
if (!html) html = '<div style="grid-column:1/-1;text-align:center;color:#9ca3af;padding:2rem;font-size:.85rem;">Keine Icons gefunden.</div>';
document.getElementById('ndIconGrid').innerHTML = html;
}
function ndSelectIcon(btn, glyph, name) {
document.querySelectorAll('.nd-icon-btn').forEach(function(b){ b.classList.remove('chosen'); });
btn.classList.add('chosen');
ndPickerSelectedIcon = {g:glyph, n:name};
document.getElementById('ndIconPickerConfirmBtn').disabled = false;
document.getElementById('ndIconPickerSelected').innerHTML =
'<span style="font-size:1.2rem;margin-right:.3rem;">'+glyph+'</span>'+name;
}
// ── Active Toggle ─────────────────────────────────────────────────────────────
async function ndToggleActive(id, btn) {
var resp = await fetch('/admin/netdiagram/devices/'+id+'/toggle', {
method:'POST',
headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
body: JSON.stringify({_csrf: document.querySelector('meta[name=csrf-token]')?.content})
});
var data = await resp.json();
if (data.ok) {
var row = document.getElementById('nd-type-row-'+id);
if (data.is_active) {
btn.textContent = 'Aktiv';
btn.className = 'nd-status-btn nd-status-active';
row.classList.remove('nd-inactive');
} else {
btn.textContent = 'Inaktiv';
btn.className = 'nd-status-btn nd-status-inactive';
row.classList.add('nd-inactive');
}
}
}
// ── Auto-fill key ─────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function() {
var labelEl = document.getElementById('ndFormLabel');
var keyEl = document.getElementById('ndFormKey');
if (labelEl && keyEl) {
labelEl.addEventListener('input', function() {
if (keyEl.readOnly) return;
keyEl.value = labelEl.value
.toLowerCase()
.replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss')
.replace(/[^a-z0-9]/g,'_').replace(/_+/g,'_').replace(/^_|_$/g,'');
});
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { ndAdminCloseModal(); ndCloseIconPicker(); }
});
});
</script>