OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

unmatched-devices.php

19 KB
<?php $this->layout('layouts/admin', [
    'page_title'         => 'Unbekannte Geräte',
    'current_admin_page' => 'assets-unmatched',
    'breadcrumbs'        => [['label' => 'Unbekannte Geräte']],
]); ?>

<!-- Übersicht -->
<div class="card card-outline card-primary mb-3">
    <div class="card-header">
        <h3 class="card-title"><i class="fas fa-question-circle"></i> <?= $this->t('Unbekannte Ger&auml;te') ?> (<?= count($devices) ?>)</h3>
        <p class="text-muted small mb-0"><?= $this->te('Ger&auml;te, die vom PowerShell-Script erkannt, aber nicht zu Assets zugeordnet wurden.') ?></p>
    </div>
    <div class="card-body table-responsive p-0">
            <?php if (empty($devices)): ?>
                <div style="padding:2rem;text-align:center;" class="text-muted">
                    <i class="fas fa-check-circle text-success"></i> <?= $this->te('Alle Ger&auml;te wurden zugeordnet.') ?>
                </div>
            <?php else: ?>
            <div class="admin-table-wrap">
                <table style="width:100%;border-collapse:collapse;font-size:0.875rem;min-width:400px;">
                    <thead>
                        <tr>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Hostname') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Quelle') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Serial Number') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Letzter Benutzer') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Hardware') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:left;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Erstmals gesehen') ?></th>
                            <th scope="col" style="padding:0.65rem 1rem;text-align:center;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--gray-500);background:var(--gray-50);border-bottom:2px solid var(--gray-200);"><?= $this->te('Aktion') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($devices as $device): ?>
                            <tr style="border-bottom:1px solid var(--gray-200);">
                                <td style="padding:0.75rem 1rem;"><strong><?= htmlspecialchars($device['hostname']) ?></strong></td>
                                <td style="padding:0.75rem 1rem;">
                                    <?php $src = $device['source'] ?? 'script'; ?>
                                    <?php if ($src === 'intune'): ?>
                                        <span style="display:inline-block;padding:0.15rem 0.5rem;border-radius:9999px;font-size:0.7rem;font-weight:700;background:var(--indigo-100);color:var(--indigo-800);"><?= $this->te('Intune') ?></span>
                                    <?php else: ?>
                                        <span style="display:inline-block;padding:0.15rem 0.5rem;border-radius:9999px;font-size:0.7rem;font-weight:700;background:var(--gray-100);color:var(--gray-500);"><?= $this->te('Script') ?></span>
                                    <?php endif; ?>
                                </td>
                                <td style="padding:0.75rem 1rem;">
                                    <?php if ($device['serial_number']): ?>
                                        <code style="background:var(--gray-100);padding:0.2rem 0.4rem;border-radius:0.25rem;"><?= htmlspecialchars($device['serial_number']) ?></code>
                                    <?php else: ?>
                                        <span style="color:var(--gray-400);">—</span>
                                    <?php endif; ?>
                                </td>
                                <td style="padding:0.75rem 1rem;">
                                    <?= $device['last_username'] ? htmlspecialchars($device['last_username']) : '—' ?>
                                </td>
                                <td style="padding:0.75rem 1rem;font-size:0.85rem;color:var(--gray-500);">
                                    <?php
                                        $specs = $device['hardware_specs'];
                                        $parts = [];
                                        // Die Angaben stammen vom Sync-Agenten und sind reine Fremddaten.
                                        if (!empty($specs['cpu_model'])) $parts[] = mb_substr((string)$specs['cpu_model'], 0, 30);
                                        if (!empty($specs['ram_gb']))    $parts[] = mb_substr((string)$specs['ram_gb'], 0, 20) . 'GB';
                                        if (!empty($specs['os_name']))   $parts[] = mb_substr((string)$specs['os_name'], 0, 60);
                                        echo $parts ? htmlspecialchars(implode(', ', $parts)) : '—';
                                    ?>
                                </td>
                                <td style="padding:0.75rem 1rem;font-size:0.85rem;color:var(--gray-500);">
                                    <?= date('d.m.Y H:i', strtotime($device['first_seen_at'])) ?>
                                </td>
                                <td style="padding:0.75rem 1rem;text-align:center;">
                                    <button type="button" class="btn btn-sm btn-primary" onclick="openAssignModal(<?= $device['id'] ?>, '<?= htmlspecialchars(addslashes($device['hostname'])) ?>', <?= (int)($device['suggested_category_id'] ?? 0) ?>)">
                                        <?= $this->te('Zuordnen') ?>
                                    </button>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php endif; ?>
        </div>
</div>

<!-- Zuordnungs-Modal -->
<div id="assignModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;align-items:center;justify-content:center;">
    <div style="background:white;border-radius:0.5rem;width:90%;max-width:600px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 25px rgba(0,0,0,0.1);">

        <!-- Header -->
        <div style="padding:1.5rem;border-bottom:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:center;">
            <h3 style="margin:0;font-size:1.125rem;"><?= $this->te('Gerät zuordnen:') ?> <code id="modalHostname"></code></h3>
            <button type="button" onclick="closeAssignModal()" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">✕</button>
        </div>

        <!-- Content -->
        <div style="padding:1.5rem;">
            <form id="assignForm" method="POST">
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                <input type="hidden" name="device_id" id="modalDeviceId">
                <input type="hidden" name="action" id="modalAction">

                <div style="margin-bottom:1.5rem;">
                    <label style="display:block;font-weight:600;margin-bottom:0.75rem;">
                        <input type="radio" name="assign_type" value="existing" checked onchange="switchTab('existing')">
                        <?= $this->te('Zu existierendem Asset zuordnen') ?>
                    </label>
                    <label style="display:block;font-weight:600;">
                        <input type="radio" name="assign_type" value="new" onchange="switchTab('new')">
                        <?= $this->te('Neues Asset erstellen') ?>
                    </label>
                </div>

                <!-- Existing Asset Tab -->
                <div id="existing-content" style="display:block;">
                    <div style="margin-bottom:1rem;">
                        <label style="display:block;font-weight:600;margin-bottom:0.5rem;"><?= $this->te('Asset suchen:') ?></label>
                        <input type="text" id="assetSearch" class="form-control" placeholder="<?= $this->te('Name, Inventarnummer, Serial...') ?>" autocomplete="off" style="width:100%;padding:0.5rem;border:1px solid var(--gray-300);border-radius:0.375rem;">
                        <span class="form-hint"><?= $this->te('Mindestens zwei Zeichen eingeben.') ?></span>
                        <div id="assetSearchResults" style="margin-top:0.75rem;"></div>
                    </div>
                </div>

                <!-- New Asset Tab -->
                <div id="new-content" style="display:none;">
                    <div style="margin-bottom:1rem;">
                        <label for="categorySelect" style="display:block;font-weight:600;margin-bottom:0.5rem;"><?= $this->te('Kategorie:') ?> <span style="color:var(--red-600);">*</span></label>
                        <select name="category_id" id="categorySelect" class="form-control" required style="width:100%;padding:0.5rem;border:1px solid var(--gray-300);border-radius:0.375rem;">
                            <option value=""><?= $this->te('— Wählen —') ?></option>
                            <?php foreach ($categories as $cat): ?>
                                <option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>

                    <div style="margin-bottom:1rem;">
                        <label for="manufacturerSelect" style="display:block;font-weight:600;margin-bottom:0.5rem;"><?= $this->te('Hersteller:') ?></label>
                        <select name="manufacturer_id" id="manufacturerSelect" class="form-control" style="width:100%;padding:0.5rem;border:1px solid var(--gray-300);border-radius:0.375rem;">
                            <option value=""><?= $this->te('— Optional —') ?></option>
                            <?php foreach ($manufacturers as $mfg): ?>
                                <option value="<?= $mfg['id'] ?>"><?= htmlspecialchars($mfg['name']) ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>

                    <div style="margin-bottom:1rem;">
                        <label for="typeSelect" style="display:block;font-weight:600;margin-bottom:0.5rem;"><?= $this->te('Modell/Typ:') ?></label>
                        <select name="type_id" id="typeSelect" class="form-control" style="width:100%;padding:0.5rem;border:1px solid var(--gray-300);border-radius:0.375rem;">
                            <option value=""><?= $this->te('— Optional —') ?></option>
                            <?php foreach ($types as $t): ?>
                                <option value="<?= (int)$t['id'] ?>"
                                        data-manufacturer="<?= (int)$t['manufacturer_id'] ?>"
                                        data-category="<?= $t['category_id'] ? (int)$t['category_id'] : '' ?>">
                                    <?= htmlspecialchars($t['manufacturer_name'] . ' – ' . $t['name']) ?>
                                </option>
                            <?php endforeach; ?>
                        </select>
                        <span class="form-hint"><?= $this->te('Wird nach Hersteller &amp; Kategorie gefiltert.') ?></span>
                    </div>
                </div>

                <!-- Buttons -->
                <div style="display:flex;gap:0.75rem;margin-top:1.5rem;justify-content:flex-end;">
                    <button type="button" class="btn btn-default" onclick="closeAssignModal()"><?= $this->te('Abbrechen') ?></button>
                    <button type="submit" class="btn btn-primary"><?= $this->te('Zuordnen') ?></button>
                </div>
            </form>
        </div>
    </div>
</div>

<script>
// Modell-/Typ-Auswahl zeigt nur Einträge, die zum gewählten Hersteller und
// zur Kategorie passen (gleiche Logik wie im Inventar-Formular).
function filterUnmatchedTypes() {
    var mfrId = document.getElementById('manufacturerSelect').value;
    var catId = document.getElementById('categorySelect').value;
    var sel   = document.getElementById('typeSelect');
    Array.from(sel.options).forEach(function(opt) {
        if (!opt.value) { opt.hidden = false; opt.disabled = false; return; }
        var mfrMatch = !mfrId || opt.dataset.manufacturer === mfrId;
        var catMatch = !catId || !opt.dataset.category || opt.dataset.category === catId;
        var ok = mfrMatch && catMatch;
        opt.hidden = !ok;
        opt.disabled = !ok;
        if (!ok && sel.value === opt.value) sel.value = '';
    });
}
document.getElementById('manufacturerSelect').addEventListener('change', filterUnmatchedTypes);
document.getElementById('categorySelect').addEventListener('change', filterUnmatchedTypes);

// ── Asset-Suche für die Zuordnung ───────────────────────────────────
// Die Treffer werden als Radioliste gerendert; ausgewählt wird über das Feld
// asset_id, das der Absende-Handler erwartet.
var assetSearchTimer = null;

function escapeHtml(wert) {
    return String(wert == null ? '' : wert).replace(/[&<>"']/g, function (z) {
        return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[z];
    });
}

function renderAssetResults(treffer) {
    var box = document.getElementById('assetSearchResults');
    if (!treffer.length) {
        box.innerHTML = '<div class="text-muted" style="font-size:0.85rem;">Keine passenden Assets gefunden.</div>';
        return;
    }
    var html = '';
    treffer.forEach(function (a) {
        var zeile2 = [a.model, a.serial_number, a.location, a.assigned_to]
            .filter(function (t) { return t; })
            .map(escapeHtml)
            .join(' · ');
        html += '<label style="display:block;padding:0.5rem;border:1px solid var(--gray-200);border-radius:0.375rem;margin-bottom:0.4rem;cursor:pointer;">'
              + '<input type="radio" name="asset_id" value="' + a.id + '" style="margin-right:0.5rem;">'
              + '<strong>' + escapeHtml(a.inventory_number || a.name) + '</strong>'
              + (a.inventory_number && a.name ? ' – ' + escapeHtml(a.name) : '')
              + (zeile2 ? '<div style="margin-left:1.35rem;font-size:0.8rem;color:var(--gray-500);">' + zeile2 + '</div>' : '')
              + '</label>';
    });
    box.innerHTML = html;
}

document.getElementById('assetSearch').addEventListener('input', function () {
    var begriff = this.value.trim();
    var box     = document.getElementById('assetSearchResults');

    clearTimeout(assetSearchTimer);
    if (begriff.length < 2) {
        box.innerHTML = '';
        return;
    }

    assetSearchTimer = setTimeout(function () {
        box.innerHTML = '<div class="text-muted" style="font-size:0.85rem;">Suche …</div>';
        fetch('/admin/inventory/unmatched-devices/asset-search?q=' + encodeURIComponent(begriff), {
            headers: { 'Accept': 'application/json' }
        })
            .then(function (r) {
                if (!r.ok) throw new Error('HTTP ' + r.status);
                return r.json();
            })
            .then(function (d) { renderAssetResults(d.results || []); })
            .catch(function () {
                box.innerHTML = '<div class="text-danger" style="font-size:0.85rem;">Suche fehlgeschlagen.</div>';
            });
    }, 250);
});

function openAssignModal(deviceId, hostname, suggestedCategoryId) {
    document.getElementById('modalDeviceId').value = deviceId;
    document.getElementById('modalHostname').textContent = hostname;
    document.getElementById('assignModal').style.display = 'flex';
    document.getElementById('assignForm').reset();
    document.getElementById('assetSearchResults').innerHTML = '';
    switchTab('existing');
    // Kategorie-Vorschlag (z. B. aus Intune-Geräteklasse) vorauswählen
    if (suggestedCategoryId) {
        var sel = document.getElementById('categorySelect');
        if (sel) sel.value = String(suggestedCategoryId);
    }
    filterUnmatchedTypes();
}

function closeAssignModal() {
    document.getElementById('assignModal').style.display = 'none';
}

function switchTab(tab) {
    const existingContent = document.getElementById('existing-content');
    const newContent = document.getElementById('new-content');

    if (tab === 'existing') {
        existingContent.style.display = 'block';
        newContent.style.display = 'none';
    } else {
        existingContent.style.display = 'none';
        newContent.style.display = 'block';
    }
}

document.getElementById('assignForm').addEventListener('submit', async (e) => {
    e.preventDefault();

    const assignType = document.querySelector('input[name="assign_type"]:checked').value;
    const deviceId = document.getElementById('modalDeviceId').value;
    const form = new FormData(document.getElementById('assignForm'));

    if (assignType === 'existing') {
        const selectedAsset = document.querySelector('input[name="asset_id"]:checked');
        if (!selectedAsset) {
            alert('Bitte wähle ein Asset aus.');
            return;
        }
        form.set('action', 'assign_existing');
    } else {
        const categoryId = document.getElementById('categorySelect').value;
        if (!categoryId) {
            alert('Bitte wähle eine Kategorie aus.');
            return;
        }
        form.set('action', 'create_new');
    }

    let data;
    try {
        const response = await fetch(`/admin/inventory/unmatched-devices/${deviceId}/assign`, {
            method: 'POST',
            headers: { 'Accept': 'application/json' },
            body: form
        });
        data = await response.json();
    } catch (e) {
        alert('Netzwerkfehler: ' + e.message);
        return;
    }

    if (data.success) {
        const msg = data.inventory_number
            ? `Gerät als Asset angelegt (${data.inventory_number}).`
            : 'Gerät erfolgreich zugeordnet!';
        alert(msg);
        location.reload();
    } else {
        alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
    }
});
</script>