Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => 'Lizenz-Sync (Microsoft Entra)',
'current_admin_page' => 'license-sync',
]); ?>
<style>
.ls-row { display:flex; align-items:flex-start; gap:.75rem; padding:.6rem 0; border-bottom:1px solid var(--slate-100); }
.ls-row:last-child { border-bottom:none; }
.ls-row .lbl { width:200px; color:var(--slate-500); font-size:.85rem; flex-shrink:0; }
.ls-row .val { flex:1; font-size:.9rem; color:var(--slate-800); }
@media (max-width: 640px) {
.ls-row { flex-direction:column; gap:.3rem; padding:.55rem 0; }
.ls-row .lbl { width:auto; font-weight:600; color:var(--slate-600); }
}
</style>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Einstellungen') ?></span></div>
<div class="detail-card-body">
<?php if (!$azure_configured): ?>
<div style="padding:.75rem 1rem; background:var(--amber-100); border:1px solid var(--amber-400); border-radius:5px; color:var(--amber-900); margin-bottom:1rem;">
<?= $this->t('<strong>Azure-Zugangsdaten fehlen.</strong> Bitte unter <a href="/admin/settings/m365" style="color:var(--amber-800);font-weight:600;">Einstellungen → M365</a> Tenant-ID, Client-ID und Client-Secret hinterlegen, bevor der Lizenz-Sync etwas tun kann.') ?>
</div>
<?php else: ?>
<div class="ls-row">
<div class="lbl"><?= $this->te('Tenant') ?></div>
<div class="val"><code><?= $this->e($tenant_id) ?></code></div>
</div>
<?php endif; ?>
<form method="POST" action="/admin/licenses/sync">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="ls-row">
<div class="lbl"><?= $this->te('Auto-Sync') ?></div>
<div class="val">
<label style="display:inline-flex; align-items:flex-start; gap:.55rem; cursor:pointer;">
<input type="checkbox" name="entra_sync_enabled" value="1" <?= $enabled ? 'checked' : '' ?> style="margin-top:.2rem;">
<span>
<strong><?= $this->te('Lizenzen automatisch aus Microsoft Entra synchronisieren') ?></strong><br>
<span style="color:var(--gray-500); font-size:.85rem;">
<?= $this->t('Wird beim regelmaessigen <code>sync-directory.php</code>-Cron mit ausgefuehrt. Liest <code>/subscribedSkus</code> aus dem Tenant und legt für jede Microsoft 365 SKU einen Eintrag im Lizenz-Modul an bzw. aktualisiert Sitze und Status. Notizen, Kosten und Vertragsverknuepfungen bleiben unangetastet.') ?>
</span>
</span>
</label>
</div>
</div>
<div class="ls-row">
<div class="lbl"><?= $this->te('Platzhalter') ?></div>
<div class="val">
<label style="display:inline-flex; align-items:flex-start; gap:.55rem; cursor:pointer;">
<input type="checkbox" name="entra_sync_assignments" value="1" <?= !empty($sync_assignments) ? 'checked' : '' ?> style="margin-top:.2rem;">
<span>
<?= $this->t('<strong>Auch übernehmen, <em>wer</em> einen Lizenzplatz hält</strong>') ?><br>
<span style="color:var(--gray-500); font-size:.85rem;">
<?= $this->t('Ohne diese Einstellung steht an der Lizenz nur die Zahl der belegten Plätze. Eingeschaltet trägt der Abgleich zusätzlich ein, welche Person welchen Platz hält – sichtbar an der Lizenz unter „Zugeordnet an". Die Angabe stammt aus <code>assignedLicenses</code> am Benutzerobjekt und kostet keinen Abruf je Person.') ?><br>
<?= $this->t('Der Abgleich ersetzt dabei <strong>nur seine eigenen</strong> Zuordnungen; von Hand eingetragene bleiben bestehen. Personen, die der Verzeichnisabgleich nicht kennt, werden übersprungen – angelegt werden sie dort, nicht hier.') ?>
<?php if (!empty($assignment_count)): ?>
<br><?= $this->te('Derzeit übernommen:') ?> <strong><?= (int)$assignment_count ?></strong> <?= $this->te('Zuordnung(en).') ?>
<?php endif; ?>
</span>
</span>
</label>
</div>
</div>
<div class="ls-row">
<div class="lbl"><?= $this->te('Voraussetzungen') ?></div>
<div class="val" style="font-size:.85rem; color:var(--gray-600);">
<?= $this->t('In der Azure-App-Registrierung als <strong>Application Permission</strong> (nicht Delegated):<br> · <code>Organization.Read.All</code> — lesender Zugriff auf <code>/subscribedSkus</code><br> · <code>User.Read.All</code> — nur für die Platzhalter-Übernahme (<code>/users</code>)<br> Admin-Consent muss erteilt sein.') ?>
</div>
</div>
<div style="margin-top:.75rem;">
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> <?= $this->te('Speichern') ?></button>
</div>
</form>
<div style="margin-top:1rem;padding:0.75rem 1rem;background:var(--blue-50);border:1px solid var(--blue-200);border-radius:5px;font-size:0.85rem;color:var(--color-primary-deep);">
<?= $this->t('<strong>Benötigte Berechtigung in der App-Registrierung:</strong> Microsoft Graph → <strong>Anwendungsberechtigung</strong> (Application, <em>nicht</em> Delegiert) <code>Organization.Read.All</code> mit erteilter Administratorzustimmung.') ?>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Manuell synchronisieren') ?></span></div>
<div class="detail-card-body">
<div class="ls-row">
<div class="lbl"><?= $this->te('Bereits synchronisiert') ?></div>
<div class="val">
<?php if ($entra_count > 0): ?>
<span class="status-pill status-pill--info"><?= $entra_count ?> Lizenz<?= $entra_count !== 1 ? 'en' : '' ?> aus Entra</span>
<a href="/licenses" style="margin-left:.5rem; color:var(--blue-600); font-size:.85rem;"><?= $this->te('Im Lizenz-Modul anzeigen →') ?></a>
<?php else: ?>
<span class="status-pill status-pill--warning"><?= $this->te('noch keine') ?></span>
<?php endif; ?>
</div>
</div>
<div class="ls-row">
<div class="lbl"><?= $this->te('Sofort-Sync') ?></div>
<div class="val">
<p style="margin:0 0 .65rem; color:var(--gray-500); font-size:.85rem;">
<?= $this->t('Loest <code>/subscribedSkus</code> einmalig sofort aus — unabhaengig vom Cron-Zeitplan und vom Personen-Sync. Ergebnis erscheint in der Lauf-Historie unten.') ?>
</p>
<form method="POST" action="/admin/licenses/sync-now"
onsubmit="return confirm('Microsoft 365 Lizenzen jetzt aus dem Entra-Tenant synchronisieren?');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-primary" <?= !$azure_configured ? 'disabled' : '' ?>>
<i class="fas fa-sync"></i> <?= $this->te('Lizenzen jetzt synchronisieren') ?>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Lauf-Historie') ?></span></div>
<div class="detail-card-body">
<?php if (empty($last_runs)): ?>
<div class="empty-state"><i class="fas fa-history"></i><p><?= $this->te('Noch keine Sync-Laeufe protokolliert.') ?></p></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th scope="col" style="width:160px;"><?= $this->te('Gestartet') ?></th>
<th scope="col" style="width:90px;"><?= $this->te('Dauer') ?></th>
<th scope="col" style="width:90px;"><?= $this->te('Status') ?></th>
<th scope="col" style="width:80px;"><?= $this->te('Items') ?></th>
<th scope="col"><?= $this->te('Meldung') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($last_runs as $r):
$started = $r['started_at'] ? date('d.m.Y H:i:s', strtotime($r['started_at'])) : '–';
$duration = '–';
if (!empty($r['started_at']) && !empty($r['finished_at'])) {
$sec = strtotime($r['finished_at']) - strtotime($r['started_at']);
$duration = $sec >= 0 ? $sec . 's' : '–';
}
$statusCls = match ($r['status']) {
'success' => 'status-pill--success',
'error' => 'status-pill--danger',
'running' => 'status-pill--warning',
default => 'status-pill--info',
};
$statusLbl = match ($r['status']) {
'success' => 'OK',
'error' => 'Fehler',
'running' => 'Laeuft',
default => $r['status'],
};
?>
<tr>
<td><?= $started ?></td>
<td><?= $duration ?></td>
<td><span class="status-pill <?= $statusCls ?>"><?= $statusLbl ?></span></td>
<td><?= (int)($r['items_processed'] ?? 0) ?></td>
<td style="max-width:520px; word-break:break-word;"><?= $this->e($r['message'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>