Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => $page_title,
'current_admin_page' => 'intune-mdm',
'breadcrumbs' => [['label' => 'Microsoft Intune']],
]); ?>
<style>
/* Nur fuer die Sync-Ergebnis-Boxen – alles andere nutzt die Standard-Admin-Styles. */
.int-result {
border: 1px solid var(--green-200);
background: var(--green-50);
color: var(--green-700);
border-radius: var(--radius, .5rem);
padding: .9rem 1.1rem;
font-size: .875rem;
line-height: 1.5;
}
.int-result.int-result-error { background:var(--red-50); border-color:var(--red-300); color:var(--red-700); }
.int-result strong { display:block; margin-bottom:.35rem; }
.int-result ul { margin:.5rem 0 0; padding-left:1.2rem; }
.int-result ul.int-errs { color:var(--red-800); }
.int-result li { margin:.1rem 0; }
#int-sync-result { display:none; margin-top:1rem; }
#int-test-result { font-size:.83rem; }
</style>
<?php if ($last_result): ?>
<div class="int-result <?= empty($last_result['errors']) ? '' : 'int-result-error' ?>" style="margin-bottom:1rem;">
<strong><?= $this->te('Letzter Sync: {zeitpunkt}', ['zeitpunkt' => $last_sync ?? '–']) ?></strong>
<?= $this->te('{anzahl} neu angelegt', ['anzahl' => (int)($last_result['created'] ?? 0)]) ?> ·
<?= $this->te('{anzahl} aktualisiert', ['anzahl' => (int)($last_result['updated'] ?? 0)]) ?> ·
<?= $this->te('{anzahl} verknüpft', ['anzahl' => (int)($last_result['linked'] ?? 0)]) ?> ·
<?= $this->te('{anzahl} unbekannt', ['anzahl' => (int)($last_result['unmatched'] ?? 0)]) ?> ·
<?= $this->te('{anzahl} dekommissioniert', ['anzahl' => (int)($last_result['decommissioned'] ?? 0)]) ?>
<?php if (!empty($last_result['renamed'])): ?>
· <?= $this->te('{anzahl} in Intune umbenannt', ['anzahl' => (int)$last_result['renamed']]) ?>
<?php endif; ?>
<?php if (!empty($last_result['unmatched_removed'])): ?>
· <?= (int)$last_result['unmatched_removed'] ?> aus Unknown-Liste entfernt
<?php endif; ?>
<?php if (($last_result['trigger'] ?? '') === 'cron'): ?>
<span style="font-size:.72rem;color:var(--gray-400);"><?= $this->te('(automatisch)') ?></span>
<?php endif; ?>
<?php if (!empty($last_result['errors'])): ?>
<ul class="int-errs">
<?php foreach (array_slice($last_result['errors'], 0, 5) as $err): ?>
<li><?= $this->e($err) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (isset($secret_expiry_days) && $secret_expiry_days !== null && $secret_expiry_days <= 30): ?>
<div class="alert <?= $secret_expiry_days < 0 ? 'alert-danger' : 'alert-warning' ?>" style="margin-bottom:1rem;font-size:.88rem;">
<i class="fas fa-key"></i>
<?php if ($secret_expiry_days < 0): ?>
<?= $this->t('<strong>Client-Secret abgelaufen</strong> (seit {tage} Tag(en)). Der Sync schlägt fehl, bis ein neues Secret hinterlegt wird.', ['tage' => abs((int)$secret_expiry_days)]) ?>
<?php else: ?>
<?= $this->t('<strong>Client-Secret läuft in {tage} Tag(en) ab.</strong> Rechtzeitig in Entra ID ein neues Geheimnis erstellen und hier eintragen.', ['tage' => (int)$secret_expiry_days]) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- ═══ Einstellungen ════════════════════════════════════════════════ -->
<form method="post" action="/admin/inventory/intune/save">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="card card-outline card-primary mb-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cloud"></i> <?= $this->te('Verbindungseinstellungen') ?></h3>
<p class="text-muted small mb-0">
<?= $this->t('Verbindung zu Microsoft Intune via Microsoft Graph. Verwendet die unter <a href="/admin/settings/m365">Einstellungen → M365</a> konfigurierte App-Registrierung; benötigt die <strong>Application-Permission</strong> <code>DeviceManagementManagedDevices.Read.All</code>.') ?>
</p>
</div>
<div class="card-body">
<!-- Zugangsdaten kommen aus den Microsoft-365-Einstellungen (wie der Verzeichnis-Sync) -->
<div style="padding:1rem;background:var(--blue-50);border:1px solid var(--blue-200);border-radius:0.5rem;font-size:0.875rem;margin-bottom:1rem;">
<strong><?= $this->te('Microsoft-365-Zugangsdaten') ?></strong><br>
<?= $this->t('Tenant-ID, Client-ID und Client-Secret werden aus <a href="/admin/settings/m365" style="color:var(--blue-600);">Einstellungen → M365</a> übernommen (gemeinsame App mit Anmeldung, Lizenz- und Verzeichnis-Sync).') ?><br>
<?php if (empty($cfg['uses_m365'])): ?>
<div style="margin-top:0.75rem;color:var(--red-600);"><?= $this->t('⚠ M365-Zugangsdaten fehlen. Bitte unter <a href="/admin/settings/m365">M365-Einstellungen</a> Tenant-ID, Client-ID und Secret hinterlegen.') ?></div>
<?php else: ?>
<div style="margin-top:0.75rem;color:var(--green-600);"><?= $this->te('✓ Tenant ID:') ?> <code><?= $this->e($cfg['tenant_id']) ?></code></div>
<?php endif; ?>
<div style="margin-top:0.75rem;padding-top:0.75rem;border-top:1px dashed var(--blue-200);">
<strong><?= $this->te('Benötigte Berechtigung in der App-Registrierung:') ?></strong>
<ul style="margin:0.4rem 0 0 1.2rem;padding:0;">
<li><?= $this->t('Microsoft Graph → <strong>Anwendungsberechtigung</strong> (Application, <em>nicht</em> Delegiert): <code>DeviceManagementManagedDevices.Read.All</code>') ?></li>
<li><?= $this->t('Anschließend <em>Administratorzustimmung erteilen</em>') ?></li>
</ul>
<small style="color:var(--gray-500);"><?= $this->te('Der Sync läuft als App (Client-Credentials) – delegierte Berechtigungen genügen nicht und führen zu „Forbidden".') ?></small>
</div>
</div>
<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.5rem;">
<button type="button" id="int-test-btn" class="btn btn-default">
<i class="fas fa-plug"></i> <?= $this->te('Verbindung testen') ?>
</button>
<button type="button" id="int-perm-btn" class="btn btn-default">
<i class="fas fa-shield-alt"></i> <?= $this->te('Berechtigungen prüfen') ?>
</button>
<span id="int-test-result"></span>
</div>
<div id="int-perm-result" style="margin-bottom:.75rem;"></div>
<hr>
<div class="form-group">
<label><?= $this->te('Betriebssysteme synchronisieren') ?></label>
<div style="display:flex;flex-wrap:wrap;gap:.5rem 1.25rem;margin-top:.3rem;">
<?php
$osList = ['Windows' => 'Windows', 'macOS' => 'macOS',
'iOS' => 'iOS / iPadOS', 'Android' => 'Android'];
foreach ($osList as $val => $label):
$checked = in_array($val, $cfg['os_filter'], true) ? 'checked' : '';
?>
<label style="display:inline-flex;align-items:center;gap:.4rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_os_filter[]" value="<?= $val ?>" <?= $checked ?>>
<span><?= $label ?></span>
</label>
<?php endforeach; ?>
</div>
<small class="form-text text-muted"><?= $this->te('Keine Auswahl = alle Betriebssysteme importieren.') ?></small>
</div>
<hr>
<div class="form-group">
<label><i class="fas fa-th-large"></i> <?= $this->te('Kategorien nach Geräteklasse') ?></label>
<div class="form-row-2">
<?php
$catDropdowns = [
['id'=>'int-cat-desktop', 'name'=>'intune_category_desktop', 'selected'=>$cfg['category_desktop'], 'label'=>'Desktop (Windows)','icon'=>'fa-desktop'],
['id'=>'int-cat-laptop', 'name'=>'intune_category_laptop', 'selected'=>$cfg['category_laptop'], 'label'=>'Notebook / Laptop','icon'=>'fa-laptop'],
['id'=>'int-cat-mac', 'name'=>'intune_category_mac', 'selected'=>$cfg['category_mac'], 'label'=>'Mac (macOS)','icon'=>'fa-laptop'],
['id'=>'int-cat-smartphone','name'=>'intune_category_smartphone', 'selected'=>$cfg['category_smartphone'],'label'=>'Smartphone','icon'=>'fa-mobile-alt'],
['id'=>'int-cat-tablet', 'name'=>'intune_category_tablet', 'selected'=>$cfg['category_tablet'], 'label'=>'Tablet','icon'=>'fa-tablet-alt'],
['id'=>'int-cat-default', 'name'=>'intune_default_category_id', 'selected'=>$cfg['default_category_id'],'label'=>'Fallback-Kategorie','icon'=>'fa-question-circle'],
];
foreach ($catDropdowns as $dd):
?>
<div class="form-group">
<label for="<?= $dd['id'] ?>"><i class="fas <?= $dd['icon'] ?>"></i> <?= $dd['label'] ?></label>
<select id="<?= $dd['id'] ?>" name="<?= $dd['name'] ?>" class="form-control">
<option value="0"><?= $this->te('– Keine Zuweisung (Fallback) –') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"
<?= $dd['selected'] == $cat['id'] ? 'selected' : '' ?>>
<?= $this->e($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
<small class="form-text text-muted">
<?= $this->t('Die Geräteklasse wird automatisch aus Betriebssystem, dem Intune-Feld <code>chassisType</code>/<code>deviceType</code> sowie dem Modellnamen ermittelt. „Fallback“ greift, wenn keine Klasse erkannt wird oder die jeweilige Kategorie leer gelassen wurde.') ?>
</small>
</div>
<div class="alert alert-info" style="font-size:.83rem;margin-bottom:0;">
<i class="fas fa-info-circle"></i>
<?= $this->t('Geräte werden zuerst über die Intune-Geräte-ID, andernfalls über die <strong>Seriennummer</strong> einem bereits vorhandenen Asset zugeordnet (manuell angelegt oder aus Relution migriert) – es entstehen keine Duplikate.') ?>
</div>
<hr>
<div class="form-group">
<label><i class="fas fa-inbox"></i> <?= $this->te('Neue / unbekannte Geräte') ?></label>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_auto_create_assets" style="margin-top:.2rem;"
<?= $cfg['auto_create_assets'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Nicht zuordenbare Geräte automatisch als Asset anlegen') ?><br>
<small class="text-muted">
<?= $this->t('<strong>Aus:</strong> Geräte, die keinem vorhandenen Asset (Geräte-ID oder Seriennummer) zugeordnet werden können, landen in der <a href="/admin/inventory/unmatched-devices">Unbekannte-Geräte-Liste</a> zur manuellen Prüfung. <strong>An:</strong> sie werden sofort mit der je Geräteklasse gewählten Kategorie als neues Asset angelegt.') ?>
</small>
</span>
</label>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;margin-top:.6rem;">
<input type="checkbox" name="intune_number_from_category" style="margin-top:.2rem;"
<?= $cfg['number_from_category'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Inventarnummer aus dem Nummernkreis der Kategorie vergeben') ?><br>
<small class="text-muted">
<?= $this->t('<strong>Aus:</strong> neue Geräte erhalten die Seriennummer als Inventarnummer. <strong>An:</strong> sie erhalten die nächste freie Nummer aus dem Nummernkreis ihrer Kategorie – wie bei der manuellen Anlage (Format je Kategorie unter <a href="/admin/inventory/categories">Kategorien</a>).') ?>
</small>
</span>
</label>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;margin-top:.6rem;">
<input type="checkbox" name="intune_writeback_name" style="margin-top:.2rem;"
<?= $cfg['writeback_name'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Vergebene Inventarnummer als Gerätename nach Intune zurückschreiben') ?><br>
<small class="text-muted">
<?= $this->t('Benennt das Gerät in Intune beim Anlegen auf die vergebene Inventarnummer um (Graph-Aktion <code>setDeviceName</code>). Erfordert zusätzlich die Application-Permission <code>DeviceManagementManagedDevices.PrivilegedOperations.All</code> mit Admin-Einwilligung. Bei Windows-Geräten wird der <strong>Computername</strong> geändert (wirksam nach Neustart, max. 15 Zeichen, keine Leer-/Sonderzeichen außer Bindestrich – das Nummernformat der Kategorie sollte dazu passen).') ?>
</small>
</span>
</label>
</div>
<hr>
<div class="form-group">
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_primary_user" style="margin-top:.2rem;"
<?= $cfg['primary_user'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Primären Benutzer übernehmen statt der registrierenden Person') ?><br>
<small class="text-muted">
<?= $this->t('Intune führt beides getrennt: Am Gerät steht, <em>wer es registriert hat</em>; der <strong>primäre Benutzer</strong> ist die Person, der es zugedacht ist. Bereitet die IT Geräte vor der Ausgabe vor, sind das zwei verschiedene – und „Zugewiesen an“ zeigt dann die IT statt der Person, die das Gerät hat.<br> Der primäre Benutzer steht nicht in der Geräteliste. OpenNIT versucht, ihn mitliefern zu lassen; geht das nicht, wird er <strong>je Gerät einzeln</strong> geholt – bei vielen Geräten dauert der Abgleich dadurch spürbar länger. Meldet Intune keinen primären Benutzer, bleibt es bei der registrierenden Person.') ?>
</small>
</span>
</label>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;margin-top:.6rem;">
<input type="checkbox" name="intune_writeback_user" style="margin-top:.2rem;"
<?= $cfg['writeback_user'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Änderungen an „Zugewiesen an“ nach Intune zurückschreiben') ?><br>
<small class="text-muted">
<?= $this->t('Ohne diesen Schalter ist das Feld bei Intune-Geräten nur lesbar, weil der nächste Abgleich eine Eingabe ohnehin überschreibt. Mit ihm geht die Änderung den umgekehrten Weg: erst nach Intune als primärer Benutzer, und nur wenn das gelingt, auch in OpenNIT.<br> Nur Personen aus dem <strong>Entra-Verzeichnis</strong> sind möglich – Intune kennt keine Personen aus LDAP oder einem CSV-Import. Erfordert dieselbe Application-Permission wie das Umbenennen (<code>DeviceManagementManagedDevices.PrivilegedOperations.All</code>).<br> Setzt den Schalter darüber voraus: Ohne das Übernehmen des primären Benutzers läse der nächste Abgleich wieder die registrierende Person und die Änderung wäre still verloren. Wird deshalb mit eingeschaltet.') ?>
</small>
</span>
</label>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;margin-top:.6rem;">
<input type="checkbox" name="intune_delete_device" style="margin-top:.2rem;"
<?= $cfg['delete_device'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Beim Löschen eines Geräts auch aus der Intune-Verwaltung entfernen') ?><br>
<small class="text-muted">
<?= $this->t('Wird ein Gerät im Inventar gelöscht, wird der Verwaltungsdatensatz in Intune mit entfernt. Das Gerät selbst wird dabei <strong>weder zurückgesetzt noch gelöscht</strong> – es ist danach nur unverwaltet und müsste neu registriert werden.<br> Gilt nur beim Löschen <strong>eines einzelnen</strong> Geräts; Sammelaktionen bleiben rein örtlich, damit ein Fehlgriff nicht auf viele Geräte im Mandanten durchschlägt. Scheitert das Entfernen in Intune, wird das Gerät auch hier <strong>nicht</strong> gelöscht – sonst wäre es in OpenNIT weg und liefe in Intune weiter.') ?>
</small>
</span>
</label>
<div style="margin:.6rem 0 0 1.6rem;">
<div style="font-size:.85rem;color:var(--gray-600);margin-bottom:.35rem;">
<?= $this->te('Vorauswahl beim Löschen – entschieden wird je Gerät:') ?>
</div>
<?php foreach (\OpenNIT\Services\IntuneSyncService::REMOVAL_ACTIONS as $aktionKey => $aktion): ?>
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;margin-bottom:.3rem;">
<input type="radio" name="intune_delete_action" value="<?= $this->e($aktionKey) ?>" style="margin-top:.25rem;"
<?= ($cfg['delete_action'] ?? 'delete') === $aktionKey ? 'checked' : '' ?>>
<span><?= $this->e($aktion['label']) ?></span>
</label>
<?php endforeach; ?>
<small class="text-muted" style="display:block;margin-top:.35rem;">
<?= $this->t('„Firmendaten entfernen“ und „Zurücksetzen“ stellen in Intune einen <strong>Auftrag</strong> ein, den das Gerät beim nächsten Kontakt ausführt – der Verwaltungsdatensatz verschwindet dort also erst danach, bei einem Gerät ohne Verbindung unter Umständen lange nicht. Das Zurücksetzen löscht <strong>alle Daten</strong> auf dem Gerät und lässt sich nicht zurücknehmen; es muss beim Löschen eigens bestätigt werden. Alle drei Wege erfordern die Application-Permission <code>DeviceManagementManagedDevices.PrivilegedOperations.All</code>.') ?>
</small>
</div>
</div>
<hr>
<div class="form-group">
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_sync_policies" style="margin-top:.2rem;"
<?= $cfg['sync_policies'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Intune-Richtlinien je Gerät laden (Compliance & Konfiguration)') ?><br>
<small class="text-muted">
<?= $this->te('Zeigt zugewiesene Richtlinien und deren Status als eigenen Tab am Asset an. Erfordert zusätzliche Graph-Aufrufe pro Gerät – bei sehr vielen Geräten läuft der Sync entsprechend länger.') ?>
</small>
</span>
</label>
</div>
<div class="form-group">
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_use_beta" style="margin-top:.2rem;"
<?= $cfg['use_beta'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Erweiterte Hardware-Infos via Graph-') ?><code>beta</code> <?= $this->te('laden') ?><br>
<small class="text-muted">
<?= $this->t('Holt zusätzliche Felder wie TPM, lokale IP, OS-Edition und (sofern verfügbar) Arbeitsspeicher aus <code>hardwareInformation</code>. Nutzt den beta-Endpoint von Microsoft Graph – Felder können je nach Gerät leer bleiben.') ?>
</small>
</span>
</label>
</div>
<div class="form-group">
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_decommission_missing" style="margin-top:.2rem;"
<?= $cfg['decommission_missing'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Geräte, die nicht mehr in Intune stehen, auf „Dekommissioniert“ setzen') ?><br>
<small class="text-muted"><?= $this->t('Achtung: Betrifft nur Assets mit <code>mdm_source = intune</code>.') ?></small>
</span>
</label>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="int_grace"><?= $this->te('Karenzzeit vor dem Ausmustern (Tage)') ?></label>
<input type="number" class="form-control form-control-sm" id="int_grace" min="0" max="365"
name="intune_decommission_grace_days" value="<?= (int)($cfg['grace_days'] ?? 0) ?>">
<small class="form-text text-muted">
<?= $this->t('Ein Gerät wird erst ausgemustert, wenn es so viele Tage nicht mehr gemeldet wurde. <strong>0</strong> mustert wie bisher sofort aus.') ?>
</small>
</div>
<div class="form-group">
<label for="int_maxpct"><?= $this->te('Höchstanteil je Lauf (%)') ?></label>
<input type="number" class="form-control form-control-sm" id="int_maxpct" min="0" max="100"
name="intune_decommission_max_percent" value="<?= (int)($cfg['max_percent'] ?? 20) ?>">
<small class="form-text text-muted">
<?= $this->t('Würde ein Lauf mehr als diesen Anteil der verwalteten Geräte ausmustern, unterbleibt die Ausmusterung und wird im Protokoll vermerkt. <strong>0</strong> schaltet die Prüfung ab.') ?>
</small>
</div>
</div>
<div class="form-group" style="margin-bottom:0;">
<label style="display:flex;align-items:flex-start;gap:.5rem;cursor:pointer;font-weight:400;">
<input type="checkbox" name="intune_sync_enabled" style="margin-top:.2rem;"
<?= $cfg['sync_enabled'] ? 'checked' : '' ?>>
<span>
<?= $this->te('Automatischen Sync aktivieren (via Cronjob)') ?><br>
<small class="text-muted">
<?= $this->te('Wird vom Scheduler-Task „Microsoft Intune Sync“ ausgeführt (Admin → Cronjobs). Alternativ') ?>
<code>/admin/inventory/intune/cron</code> <?= $this->te('aufrufen.') ?>
</small>
</span>
</label>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> <?= $this->te('Einstellungen speichern') ?>
</button>
</div>
</form>
<!-- ═══ Manueller Sync ════════════════════════════════════════════════ -->
<?php
/**
* Welche Graph-Berechtigungen die App-Registrierung braucht.
*
* Abgeleitet aus den Aufrufen, die der Abgleich tatsaechlich macht. Die Liste
* richtet sich nach den Schaltern oben: Was ausgeschaltet ist, braucht seine
* Berechtigung nicht.
*/
$intunePflicht = [
[
'recht' => 'DeviceManagementManagedDevices.Read.All',
'wofuer' => 'Geräte und ihre Angaben lesen – die Grundlage jedes Abgleichs.',
'noetig' => true,
],
[
'recht' => 'User.Read.All',
'wofuer' => 'Die am Gerät hinterlegte Person auflösen (Name und E-Mail).',
'noetig' => !empty($cfg['primary_user']),
],
[
'recht' => 'DeviceManagementManagedDevices.ReadWrite.All',
'wofuer' => 'Die Zuordnung der Person zurückschreiben und ein Gerät aus der Verwaltung entfernen.',
'noetig' => !empty($cfg['writeback_user'])
|| (!empty($cfg['delete_device']) && ($cfg['delete_action'] ?? 'delete') === 'delete'),
],
[
'recht' => 'DeviceManagementManagedDevices.PrivilegedOperations.All',
'wofuer' => 'Gerätenamen setzen sowie „Firmendaten entfernen“ und „Auf Werkseinstellungen zurücksetzen“ auslösen.',
'noetig' => !empty($cfg['writeback_name'])
|| (!empty($cfg['delete_device']) && in_array($cfg['delete_action'] ?? 'delete', ['retire', 'wipe'], true)),
],
[
'recht' => 'DeviceManagementConfiguration.Read.All',
'wofuer' => 'Zugewiesene Richtlinien und Konfigurationsprofile mitlesen.',
'noetig' => !empty($cfg['sync_policies']),
],
];
?>
<div class="card card-outline card-primary mb-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-key"></i> <?= $this->te('Benötigte Berechtigungen') ?></h3>
<p class="text-muted small mb-0">
<?= $this->t('Als <strong>Anwendungsberechtigungen</strong> in der App-Registrierung (Entra ID → App-Registrierungen → API-Berechtigungen → Microsoft Graph), jeweils mit <strong>Administratorzustimmung</strong>.') ?>
</p>
</div>
<div class="card-body">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr><th scope="col"><?= $this->te('Berechtigung') ?></th><th scope="col"><?= $this->te('Wofür') ?></th><th scope="col" style="white-space:nowrap;"><?= $this->te('Jetzt nötig') ?></th></tr>
</thead>
<tbody>
<?php foreach ($intunePflicht as $z): ?>
<tr<?= $z['noetig'] ? '' : ' style="opacity:.55;"' ?>>
<td><code><?= $this->e($z['recht']) ?></code></td>
<td><?= $z['wofuer'] ?></td>
<td>
<?php if ($z['noetig']): ?>
<span class="status-pill status-pill--success"><?= $this->te('ja') ?></span>
<?php else: ?>
<span class="status-pill status-pill--neutral"><?= $this->te('nur bei Bedarf') ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<p class="text-muted small" style="margin:.75rem 0 0;line-height:1.6;">
<?= $this->t('„Nur bei Bedarf“ heißt: Die zugehörige Einstellung ist hier ausgeschaltet. Schalten Sie sie ein, muss die Berechtigung vorher erteilt sein – sonst antwortet Intune mit <code>Forbidden</code>. Das Zurücksetzen auf Werkseinstellungen ist eine privilegierte Aktion und verlangt dieselbe Berechtigung wie das Entfernen der Firmendaten.') ?>
</p>
</div>
</div>
<div class="card card-outline card-primary mb-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-sync-alt"></i> <?= $this->te('Manueller Sync') ?></h3>
<p class="text-muted small mb-0">
<?= $this->t('<strong>Vorschau (Probelauf)</strong> zeigt, was ein Sync tun würde, ohne etwas zu speichern – ideal vor dem ersten echten Lauf. <strong>Sync jetzt starten</strong> führt den Abgleich tatsächlich aus.') ?>
</p>
</div>
<div class="card-body">
<?php $intDisabled = empty($cfg['tenant_id']) || empty($cfg['client_id']) || empty($cfg['client_secret']); ?>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;">
<button type="button" id="int-preview-btn" class="btn btn-default"
<?= $intDisabled ? 'disabled title="Bitte zuerst Verbindung konfigurieren"' : '' ?>>
<i class="fas fa-search"></i> <?= $this->te('Vorschau (Probelauf)') ?>
</button>
<button type="button" id="int-sync-btn" class="btn btn-success"
<?= $intDisabled ? 'disabled title="Bitte zuerst Verbindung konfigurieren"' : '' ?>>
<i class="fas fa-play"></i> <?= $this->te('Sync jetzt starten') ?>
</button>
</div>
<div id="int-sync-result"></div>
</div>
</div>
<script>
(function () {
// ── Verbindungstest ────────────────────────────────────────────
document.getElementById('int-test-btn')?.addEventListener('click', function () {
var btn = this;
var result = document.getElementById('int-test-result');
btn.disabled = true;
btn.innerHTML = 'Teste…';
result.textContent = '';
result.style.color = '#6b7280';
fetch('/admin/inventory/intune/test')
.then(function (r) { return r.json(); })
.then(function (d) {
result.textContent = d.ok ? '✓ ' + d.message : '✗ ' + d.message;
result.style.color = d.ok ? '#15803d' : '#b91c1c';
})
.catch(function () {
result.textContent = '✗ Verbindungsfehler';
result.style.color = '#b91c1c';
})
.finally(function () {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-plug"></i> Verbindung testen';
});
});
// ── Berechtigungen prüfen ──────────────────────────────────────
// Gefragt wird der Anspruch "roles" im App-Token: Dort steht genau das,
// was vergeben UND bestätigt ist.
document.getElementById('int-perm-btn')?.addEventListener('click', function () {
var btn = this;
var box = document.getElementById('int-perm-result');
btn.disabled = true;
btn.innerHTML = <?= json_encode($this->t('Prüfe …'), JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
box.innerHTML = '';
fetch('/admin/inventory/intune/permissions')
.then(function (r) { return r.json(); })
.then(function (d) {
var html = '';
if (!d.erreichbar) {
html = '<div class="int-result-error"><strong>✗ ' + esc(d.meldung) + '</strong></div>';
} else if (d.ok) {
html = '<div style="color:var(--green-700);"><strong>✓ ' + esc(d.meldung) + '</strong></div>';
} else {
html = '<div class="int-result-error"><strong>✗ ' + esc(d.meldung) + '</strong><ul style="margin:.4rem 0 0 1.1rem;">';
(d.fehlend || []).forEach(function (f) {
html += '<li><code>' + esc(f.name) + '</code> – ' + esc(f.zweck) + '</li>';
});
html += '</ul><p style="margin:.5rem 0 0;">'
+ <?= json_encode($this->t('In Entra unter <em>App-Registrierungen → API-Berechtigungen</em> als <strong>Anwendungsberechtigung</strong> eintragen und anschließend die Administrator-Zustimmung erteilen. Ohne die Zustimmung bleibt eine eingetragene Berechtigung wirkungslos.'), JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT) ?>
+ '</p></div>';
}
if (d.gewaehrt && d.gewaehrt.length) {
html += '<details style="margin-top:.5rem;"><summary style="cursor:pointer;color:var(--gray-600);">'
+ <?= json_encode($this->t('Vergebene Berechtigungen anzeigen'), JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT) ?>
+ ' (' + d.gewaehrt.length + ')</summary><ul style="margin:.4rem 0 0 1.1rem;">';
d.gewaehrt.forEach(function (g) { html += '<li><code>' + esc(g) + '</code></li>'; });
html += '</ul></details>';
}
box.innerHTML = html;
})
.catch(function () {
box.innerHTML = '<div class="int-result-error">✗ '
+ <?= json_encode($this->t('Die Prüfung konnte nicht ausgeführt werden.'), JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT) ?>
+ '</div>';
})
.finally(function () {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-shield-alt"></i> '
+ <?= json_encode($this->t('Berechtigungen prüfen'), JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
});
});
// Fremde Angaben gehoeren maskiert in die Seite.
function esc(t) {
var d = document.createElement('div');
d.textContent = t === null || t === undefined ? '' : String(t);
return d.innerHTML;
}
// ── Gemeinsame Ergebnis-Darstellung ───────────────────────────
function renderResult(d, isPreview) {
var result = document.getElementById('int-sync-result');
var cls = d.ok ? '' : 'int-result-error';
var title = isPreview
? 'Probelauf – so würde der Sync laufen (' + d.synced_at + ')'
: 'Sync abgeschlossen – ' + d.synced_at;
var verb = isPreview ? ' würde(n) ' : ' ';
var html = '<div class="int-result ' + cls + '">'
+ '<strong>' + escHtml(title) + '</strong>'
+ d.created + verb + 'neu angelegt · '
+ d.updated + verb + 'aktualisiert · '
+ (d.linked || 0) + verb + 'verknüpft · '
+ (d.unmatched || 0) + verb + 'unbekannt · '
+ d.decommissioned + verb + 'dekommissioniert'
+ (d.renamed ? ' · ' + d.renamed + verb + 'in Intune umbenannt' : '')
+ (d.unmatched_removed ? ' · ' + d.unmatched_removed + ' aus Unknown-Liste entfernt' : '')
+ (typeof d.skipped !== 'undefined' && d.skipped ? ' · ' + d.skipped + ' übersprungen' : '');
if (d.message && !d.ok) { html += '<div style="margin-top:.4rem;">' + escHtml(d.message) + '</div>'; }
if (d.log && d.log.length) {
html += '<ul>';
d.log.forEach(function (l) { html += '<li>' + escHtml(l) + '</li>'; });
html += '</ul>';
}
if (d.errors && d.errors.length) {
html += '<ul class="int-errs">';
d.errors.slice(0, 10).forEach(function (e) { html += '<li>' + escHtml(e) + '</li>'; });
html += '</ul>';
}
html += '</div>';
result.innerHTML = html;
result.style.display = 'block';
}
function runIntuneJob(url, btn, busyLabel, idleLabel, isPreview) {
var result = document.getElementById('int-sync-result');
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + busyLabel;
result.style.display = 'none';
var params = new URLSearchParams();
params.append('_csrf', document.querySelector('meta[name="csrf-token"]')?.content ?? '');
fetch(url, {
method: 'POST', body: params,
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (d) { renderResult(d, isPreview); })
.catch(function () {
result.innerHTML = '<div class="int-result int-result-error">Verbindungsfehler.</div>';
result.style.display = 'block';
})
.finally(function () {
btn.disabled = false;
btn.innerHTML = orig;
});
}
// ── Vorschau / Probelauf ──────────────────────────────────────
document.getElementById('int-preview-btn')?.addEventListener('click', function () {
runIntuneJob('/admin/inventory/intune/preview', this, 'Probelauf läuft…', null, true);
});
// ── Manueller Sync ────────────────────────────────────────────
document.getElementById('int-sync-btn')?.addEventListener('click', function () {
runIntuneJob('/admin/inventory/intune/sync', this, 'Sync läuft…', null, false);
});
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
})();
</script>