OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

31 KB
<?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">&#128257;</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">&#9881;</span>
            <div>
                <h2><?= $this->te('Gerätetypen') ?></h2>
                <p class="settings-card-desc"><?= $this->te('{anzahl} Typ(en) vorhanden &middot; 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 &quot;<?= htmlspecialchars(addslashes($t['label'])) ?>&quot; 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('&#8592; 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') ?>">&times;</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') ?>">&times;</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>