Geräte-Tab: Suche, Detail-Panel und Aktionen
Neuer Haupttab "Geräte" mit Geräteliste (filterbar nach OS/Compliance), Detail-Panel mit vollständigen Gerätedaten und Aktions-Buttons: Sync, Neustart, Sperren, Diagnose, BitLocker-Rotation, Retire. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+240
-1
@@ -4390,6 +4390,7 @@ function switchMainView(view) {
|
||||
const layout = document.getElementById('layout');
|
||||
const groupView = document.getElementById('groupMgmtView');
|
||||
const reportView = document.getElementById('appReportView');
|
||||
const devView = document.getElementById('devView');
|
||||
document.querySelectorAll('#topbarNav .nav-tab').forEach(t => {
|
||||
t.classList.toggle('active', t.dataset.view === view);
|
||||
t.setAttribute('aria-selected', t.dataset.view === view ? 'true' : 'false');
|
||||
@@ -4397,18 +4398,20 @@ function switchMainView(view) {
|
||||
layout .classList.toggle('hidden', view !== 'apps');
|
||||
groupView .classList.toggle('hidden', view !== 'groups');
|
||||
reportView.classList.toggle('hidden', view !== 'report');
|
||||
devView .classList.toggle('hidden', view !== 'devices');
|
||||
}
|
||||
|
||||
document.querySelectorAll('#topbarNav .nav-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const view = btn.dataset.view;
|
||||
if ((view === 'groups' || view === 'report') && !State.connected) {
|
||||
if ((view === 'groups' || view === 'report' || view === 'devices') && !State.connected) {
|
||||
toast('Bitte zuerst verbinden.', 'warn');
|
||||
return;
|
||||
}
|
||||
switchMainView(view);
|
||||
if (view === 'groups') openGroupExportModal();
|
||||
if (view === 'report' && ReportState.items.length === 0) loadAppReport();
|
||||
if (view === 'devices') { devLoadAll(); setTimeout(() => document.getElementById('devSearch').focus(), 80); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5222,3 +5225,239 @@ function notifyCsvImportGroupSelected(groupId, groupName) {
|
||||
if (CsvImport.resolved.length > 0) renderCsvPreview();
|
||||
else updateCsvImportNote();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Geräte-Tab
|
||||
// ============================================================
|
||||
const DevState = {
|
||||
items: [],
|
||||
filtered: [],
|
||||
selected: null,
|
||||
sortCol: 'deviceName',
|
||||
sortAsc: true,
|
||||
loaded: false
|
||||
};
|
||||
|
||||
async function devLoadAll() {
|
||||
if (DevState.loaded) return;
|
||||
devShowLoading(true);
|
||||
try {
|
||||
const data = await api('/api/devices');
|
||||
DevState.items = data.items || [];
|
||||
DevState.loaded = true;
|
||||
devApplyFilters();
|
||||
} catch (e) {
|
||||
toast('Geräte konnten nicht geladen werden: ' + e, 'error');
|
||||
} finally {
|
||||
devShowLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function devShowLoading(on) {
|
||||
document.getElementById('devLoading').classList.toggle('hidden', !on);
|
||||
document.getElementById('devEmpty').classList.add('hidden');
|
||||
}
|
||||
|
||||
function devApplyFilters() {
|
||||
const q = document.getElementById('devSearch').value.toLowerCase();
|
||||
const os = document.getElementById('devOsFilter').value;
|
||||
const cmp = document.getElementById('devComplianceFilter').value;
|
||||
|
||||
DevState.filtered = DevState.items.filter(d => {
|
||||
if (os && d.OS !== os) return false;
|
||||
if (cmp && d.ComplianceState !== cmp) return false;
|
||||
if (q) {
|
||||
const hay = `${d.DeviceName} ${d.UserDisplayName} ${d.UserPrincipalName} ${d.SerialNumber}`.toLowerCase();
|
||||
if (!hay.includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
devSort();
|
||||
}
|
||||
|
||||
function devSort() {
|
||||
const col = DevState.sortCol;
|
||||
const asc = DevState.sortAsc;
|
||||
DevState.filtered.sort((a, b) => {
|
||||
const av = (a[col] || '').toString().toLowerCase();
|
||||
const bv = (b[col] || '').toString().toLowerCase();
|
||||
return asc ? av.localeCompare(bv) : bv.localeCompare(av);
|
||||
});
|
||||
devRender();
|
||||
}
|
||||
|
||||
function devRender() {
|
||||
const tbody = document.getElementById('devTableBody');
|
||||
const empty = document.getElementById('devEmpty');
|
||||
document.getElementById('devCount').textContent = DevState.filtered.length;
|
||||
|
||||
if (DevState.filtered.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
// Sort-Icons
|
||||
document.querySelectorAll('#devTable th.sortable').forEach(th => {
|
||||
const ico = th.querySelector('.sort-ico');
|
||||
if (th.dataset.col === DevState.sortCol) {
|
||||
ico.textContent = DevState.sortAsc ? ' ▲' : ' ▼';
|
||||
} else {
|
||||
ico.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
let html = '';
|
||||
for (const d of DevState.filtered) {
|
||||
const sync = d.LastSync ? new Date(d.LastSync).toLocaleDateString('de-DE') : '—';
|
||||
const cmpCls = d.ComplianceState === 'compliant' ? 'dev-badge-ok'
|
||||
: d.ComplianceState === 'noncompliant' ? 'dev-badge-err' : 'dev-badge-unk';
|
||||
const sel = DevState.selected && DevState.selected.Id === d.Id ? ' class="dev-row-selected"' : '';
|
||||
html += `<tr data-id="${escapeHtml(d.Id)}"${sel}>
|
||||
<td>${escapeHtml(d.DeviceName || '—')}</td>
|
||||
<td>${escapeHtml(d.UserDisplayName || '—')}</td>
|
||||
<td>${escapeHtml(d.OS || '—')}</td>
|
||||
<td><span class="dev-badge ${cmpCls}">${escapeHtml(d.ComplianceState || '—')}</span></td>
|
||||
<td>${sync}</td>
|
||||
</tr>`;
|
||||
}
|
||||
tbody.innerHTML = html;
|
||||
|
||||
tbody.querySelectorAll('tr').forEach(tr => {
|
||||
tr.addEventListener('click', () => {
|
||||
const id = tr.dataset.id;
|
||||
const device = DevState.filtered.find(d => d.Id === id);
|
||||
if (device) devSelectDevice(device);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function devSelectDevice(device) {
|
||||
DevState.selected = device;
|
||||
// Auswahl visuell
|
||||
document.querySelectorAll('#devTableBody tr').forEach(tr =>
|
||||
tr.classList.toggle('dev-row-selected', tr.dataset.id === device.Id));
|
||||
|
||||
// Detail-Panel anzeigen mit Basis-Infos sofort
|
||||
devRenderDetail(device);
|
||||
|
||||
// Vollständige Details nachladen
|
||||
try {
|
||||
const full = await api(`/api/devices/${encodeURIComponent(device.Id)}`);
|
||||
devRenderDetail(full);
|
||||
} catch (e) {
|
||||
// Basis-Infos bleiben
|
||||
}
|
||||
}
|
||||
|
||||
function devRenderDetail(d) {
|
||||
const panel = document.getElementById('devDetail');
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
document.getElementById('devDetailName').textContent = d.DeviceName || '—';
|
||||
document.getElementById('devDetailSub').textContent = `${d.Manufacturer || ''} ${d.Model || ''}`.trim() || '—';
|
||||
|
||||
const sync = d.LastSync ? new Date(d.LastSync).toLocaleString('de-DE') : '—';
|
||||
const enrolled = d.EnrolledDateTime ? new Date(d.EnrolledDateTime).toLocaleDateString('de-DE') : '—';
|
||||
const storage = (d.TotalStorageGB != null)
|
||||
? `${d.FreeStorageGB} GB frei / ${d.TotalStorageGB} GB gesamt` : null;
|
||||
|
||||
const rows = [
|
||||
['Benutzer', d.UserDisplayName],
|
||||
['UPN', d.UserPrincipalName],
|
||||
['OS', d.OS ? `${d.OS} ${d.OSVersion || ''}`.trim() : null],
|
||||
['Compliance', d.ComplianceState],
|
||||
['Seriennummer', d.SerialNumber],
|
||||
['IMEI', d.Imei],
|
||||
['Wi-Fi MAC', d.WiFiMac],
|
||||
['Speicher', storage],
|
||||
['Eingeschrieben', enrolled],
|
||||
['Letzter Sync', sync],
|
||||
['Join-Typ', d.JoinType],
|
||||
['Besitzertyp', d.OwnerType],
|
||||
['Azure AD ID', d.AzureADDeviceId],
|
||||
];
|
||||
|
||||
let html = '';
|
||||
for (const [label, val] of rows) {
|
||||
if (!val) continue;
|
||||
html += `<dt>${escapeHtml(label)}</dt><dd>${escapeHtml(String(val))}</dd>`;
|
||||
}
|
||||
document.getElementById('devInfoGrid').innerHTML = html;
|
||||
|
||||
// Aktionen: BitLocker nur für Windows
|
||||
const isWin = (d.OS || '').toLowerCase() === 'windows';
|
||||
const bitlockerBtn = document.getElementById('devBtnBitlocker');
|
||||
if (bitlockerBtn) bitlockerBtn.style.display = isWin ? '' : 'none';
|
||||
}
|
||||
|
||||
// Aktion ausführen
|
||||
document.getElementById('devActions')?.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.dev-action-btn');
|
||||
if (!btn || !DevState.selected) return;
|
||||
const action = btn.dataset.action;
|
||||
if (!action) return;
|
||||
|
||||
const labels = {
|
||||
syncDevice: 'Sync',
|
||||
rebootNow: 'Neustart',
|
||||
remoteLock: 'Sperren',
|
||||
collectDiagnostics: 'Diagnose sammeln',
|
||||
rotateBitLockerKeys: 'BitLocker-Schlüssel rotieren',
|
||||
retire: 'Retire (MDM entfernen)'
|
||||
};
|
||||
const isDangerous = action === 'retire';
|
||||
const devName = DevState.selected.DeviceName || DevState.selected.Id;
|
||||
const msg = isDangerous
|
||||
? `ACHTUNG: "${labels[action]}" auf "${devName}" ausführen?\n\nDas MDM-Profil wird entfernt — Gerät wird nicht mehr verwaltet.`
|
||||
: `"${labels[action]}" auf "${devName}" ausführen?`;
|
||||
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const origText = btn.textContent.trim();
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
await api(`/api/devices/${encodeURIComponent(DevState.selected.Id)}/action`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
toast(`${labels[action]}: Aktion wurde gesendet`, 'ok');
|
||||
if (action === 'retire') {
|
||||
// Gerät aus Liste entfernen
|
||||
DevState.items = DevState.items.filter(d => d.Id !== DevState.selected.Id);
|
||||
DevState.selected = null;
|
||||
document.getElementById('devDetail').classList.add('hidden');
|
||||
devApplyFilters();
|
||||
}
|
||||
} catch (e) {
|
||||
toast(`Fehler bei ${labels[action]}: ${e}`, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
}
|
||||
});
|
||||
|
||||
// Such- und Filter-Events
|
||||
let devSearchDebounce = null;
|
||||
document.getElementById('devSearch').addEventListener('input', () => {
|
||||
clearTimeout(devSearchDebounce);
|
||||
devSearchDebounce = setTimeout(devApplyFilters, 250);
|
||||
});
|
||||
document.getElementById('devOsFilter').addEventListener('change', devApplyFilters);
|
||||
document.getElementById('devComplianceFilter').addEventListener('change', devApplyFilters);
|
||||
|
||||
// Sortierung
|
||||
document.querySelectorAll('#devTable th.sortable').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
if (DevState.sortCol === th.dataset.col) {
|
||||
DevState.sortAsc = !DevState.sortAsc;
|
||||
} else {
|
||||
DevState.sortCol = th.dataset.col;
|
||||
DevState.sortAsc = true;
|
||||
}
|
||||
devSort();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user