OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

sync.php

10 KB
<?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 &rarr; 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 &ndash; 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 &ndash; 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> &middot; <code>Organization.Read.All</code> &mdash; lesender Zugriff auf <code>/subscribedSkus</code><br> &middot; <code>User.Read.All</code> &mdash; 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 &rarr; <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 &rarr;') ?></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 &mdash; 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>