OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

glpi-import.php

43 KB
<?php $this->layout('layouts/admin', [
    'page_title'         => $page_title,
    'current_admin_page' => $current_admin_page ?? 'assets-glpi-import',
    'breadcrumbs'        => [['label' => 'GLPI Import']],
]); ?>

<style>
/* Modul-spezifische Stile – Tabellen/Pills kommen aus app.css */
.data-table tr.exists td { opacity: 0.6; background: var(--color-background,var(--gray-50)); }
/* Drucker-Pill (rosa, keine Standard-Variante) */
.glpi-pill-printer { background: var(--pink-100); color: var(--pink-800); }
.glpi-result-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; margin: 0 0 1.25rem; }
.glpi-result-card { padding: 1.25rem 1.5rem; border-radius: 0.5rem; text-align: center; }
.glpi-result-card .num { font-size: 2.25rem; font-weight: 800; line-height: 1; }
.glpi-result-card .lbl { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
.glpi-rc-imported { background: var(--emerald-100); color: var(--emerald-800); }
.glpi-rc-skipped  { background: var(--amber-100); color: var(--amber-800); }
.glpi-rc-errors   { background: var(--red-100); color: var(--red-800); }
.glpi-error-list  { background: var(--red-50); border: 1px solid var(--red-200); border-radius: 0.5rem; padding: 0.75rem 1rem; font-size: 0.82rem; color: var(--red-900); }
.glpi-error-list li { margin-bottom: 0.25rem; }
.glpi-radio-group { display: flex; gap: 1.25rem; flex-wrap: wrap; margin-top: 0.35rem; }
.glpi-radio-group label { font-size: 0.88rem; cursor: pointer; display: flex; align-items: center; gap: 0.4rem; }
.glpi-check-group { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.35rem; }
.glpi-check-group label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.88rem; cursor: pointer; }

</style>

    <?php if (!empty($error)): ?>
    <div class="alert alert-danger" style="margin-bottom:1rem;">
        <strong><?= $this->te('Fehler:') ?></strong> <?= $this->e($error) ?>
    </div>
    <?php endif; ?>

    <?php $flashes = $this->flash(); foreach ($flashes as $flash): ?>
    <div class="alert alert-<?= $flash['type'] === 'error' ? 'danger' : $flash['type'] ?>" style="margin-bottom:1rem;">
        <?= $this->e($flash['message']) ?>
    </div>
    <?php endforeach; ?>

    <!-- ================================================================
         SCHRITT 1: Konfigurationsformular
         ================================================================ -->
    <?php if ($step === 'form'):
        $sc = $saved_credentials ?? [];
        $hasSaved   = !empty($sc['has_saved']);
        $savedAuth  = $sc['auth_method'] ?? 'token';
        // Effective auth method: form_data overrides saved
        $effectiveAuth = $form_data['auth_method'] ?? ($hasSaved ? $savedAuth : 'token');
    ?>

    <?php if ($hasSaved): ?>
    <div style="background:var(--emerald-50);border:1px solid var(--emerald-300);border-radius:0.5rem;padding:0.75rem 1rem;margin-bottom:1rem;display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
        <span style="font-size:1.1rem;">&#x1F512;</span>
        <span style="font-size:0.88rem;color:var(--emerald-800);flex:1;">
            <?= $this->t('<strong>Zugangsdaten gespeichert</strong> &ndash; Verbindung zu <code>{url}</code> ist hinterlegt. Leere Felder werden automatisch mit den gespeicherten Daten aufgef&uuml;llt.', ['url' => $this->e($sc['glpi_url'])]) ?>
        </span>
        <form method="POST" action="/admin/inventory/import/glpi/clear-credentials" style="margin:0;">
            <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">
            <button type="submit" class="btn btn-default" style="font-size:0.78rem;padding:0.25rem 0.65rem;border-color:var(--red-600);color:var(--red-600);"
                    onclick="return confirm('Gespeicherte GLPI-Zugangsdaten wirklich l\u00f6schen?')">
                <?= $this->te('&#x1F5D1; Zugangsdaten l&ouml;schen') ?>
            </button>
        </form>
    </div>
    <?php endif; ?>

    <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('GLPI-Verbindung konfigurieren') ?></h3>
            <p class="text-muted small mb-0"><?= $this->te('Importiert Computer, Netzwerkger&auml;te, Drucker und Smartphones aus GLPI in das lokale Inventar.') ?></p>
        </div>
        <div class="card-body">
            <form method="POST" action="/admin/inventory/import/glpi/connect">
                <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">

                <div class="form-row-2">
                    <div class="form-group">
                        <label for="glpi_url"><?= $this->te('GLPI URL') ?> <span class="text-danger">*</span></label>
                        <input type="url" id="glpi_url" name="glpi_url" class="form-control"
                               placeholder="https://glpi.example.com"
                               value="<?= $this->e($form_data['glpi_url'] ?? $glpi_url ?? '') ?>"
                               required>
                        <small class="form-text text-muted"><?= $this->t('Basis-URL Ihrer GLPI-Installation (ohne <code>/apirest.php</code>)') ?></small>
                    </div>
                    <div class="form-group">
                        <label for="app_token"><?= $this->te('App Token') ?> <span style="font-weight:400;font-size:0.8rem;"><?= $this->te('(optional)') ?></span></label>
                        <input type="password" id="app_token" name="app_token" class="form-control"
                               placeholder="<?= $hasSaved && $sc['app_token'] !== '' ? '(gespeichert – leer lassen um beizubehalten)' : 'Falls in GLPI unter Administration → API konfiguriert' ?>"
                               value="">
                        <small class="form-text text-muted"><?= $this->t('Kann in GLPI unter <em>Administration &rarr; API</em> angelegt werden. Wird verschl&uuml;sselt gespeichert.') ?></small>
                    </div>
                </div>

                <hr style="border:none;border-top:1px solid var(--color-border,var(--gray-200));margin:1.25rem 0;">

                <div class="form-group">
                    <label><?= $this->te('Authentifizierungsmethode') ?> <span class="text-danger">*</span></label>
                    <div class="glpi-radio-group">
                        <label>
                            <input type="radio" name="auth_method" value="token"
                                <?= $effectiveAuth !== 'credentials' ? 'checked' : '' ?>
                                onchange="toggleAuth(this.value)">
                            <?= $this->te('User Token') ?>
                        </label>
                        <label>
                            <input type="radio" name="auth_method" value="credentials"
                                <?= $effectiveAuth === 'credentials' ? 'checked' : '' ?>
                                onchange="toggleAuth(this.value)">
                            <?= $this->te('Benutzername / Passwort') ?>
                        </label>
                    </div>
                </div>

                <div id="auth-token" class="form-group" <?= $effectiveAuth === 'credentials' ? 'style="display:none;"' : '' ?>>
                    <label for="user_token"><?= $this->te('User Token') ?></label>
                    <input type="password" id="user_token" name="user_token" class="form-control"
                           placeholder="<?= $hasSaved && $sc['user_token'] !== '' ? '(gespeichert – leer lassen um beizubehalten)' : 'GLPI User Token aus Benutzerprofil → API Token' ?>"
                           value="">
                    <small class="form-text text-muted"><?= $this->t('Zu finden in GLPI unter <em>Benutzerprofil &rarr; API Token</em>. Wird verschl&uuml;sselt gespeichert.') ?></small>
                </div>

                <div id="auth-credentials" <?= $effectiveAuth === 'credentials' ? '' : 'style="display:none;"' ?>>
                    <div class="form-row-2">
                        <div class="form-group">
                            <label for="username"><?= $this->te('Benutzername') ?></label>
                            <input type="text" id="username" name="username" class="form-control"
                                   placeholder="<?= $this->te('GLPI Benutzername') ?>"
                                   value="<?= $this->e($form_data['username'] ?? $sc['username'] ?? '') ?>">
                        </div>
                        <div class="form-group">
                            <label for="password"><?= $this->te('Passwort') ?></label>
                            <input type="password" id="password" name="password" class="form-control"
                                   placeholder="<?= $hasSaved && $sc['password'] !== '' ? '(gespeichert – leer lassen um beizubehalten)' : 'GLPI Passwort' ?>">
                        </div>
                    </div>
                    <small class="form-text text-muted" style="display:block;margin-top:-0.5rem;margin-bottom:1rem;"><?= $this->te('Benutzername und Passwort werden verschl&uuml;sselt gespeichert.') ?></small>
                </div>

                <hr style="border:none;border-top:1px solid var(--color-border,var(--gray-200));margin:1.25rem 0;">

                <div class="form-row-2">
                    <div class="form-group">
                        <label for="default_status"><?= $this->te('Standard-Status f&uuml;r importierte Assets') ?></label>
                        <select name="default_status" id="default_status" class="form-control">
                            <option value="active"><?= $this->te('Aktiv') ?></option>
                            <option value="storage"><?= $this->te('Lager') ?></option>
                            <option value="decommissioned"><?= $this->te('Ausgemustert') ?></option>
                        </select>
                        <small class="form-text text-muted"><?= $this->te('Wird verwendet, wenn GLPI keinen auswertbaren Status liefert.') ?></small>
                    </div>
                    <div class="form-group" style="align-self:center;padding-top:1.25rem;">
                        <label class="checkbox-inline">
                            <input type="checkbox" name="skip_existing" value="1" checked>
                            <?= $this->te('Bereits vorhandene Assets &uuml;berspringen') ?>
                        </label>
                        <small class="form-text text-muted"><?= $this->te('Erkennung per Seriennummer.') ?></small>
                    </div>
                </div>

                <div class="form-actions" style="flex-wrap:wrap;gap:0.75rem;">
                    <button type="submit" formaction="/admin/inventory/import/glpi/connect" class="btn btn-primary">
                        <?= $this->te('&#x1F50C; Verbinden &amp; Ger&auml;tetypen laden') ?>
                    </button>
                    <button type="submit" formaction="/admin/inventory/import/glpi/locations-preview" class="btn btn-default" style="border-color:var(--blue-600);color:var(--blue-600);">
                        <?= $this->te('&#x1F4CD; Standorte aus GLPI importieren') ?>
                    </button>
                    <a href="/inventory" class="btn btn-default"><?= $this->te('Abbrechen') ?></a>
                </div>
            </form>
        </div>
    </div>

    <!-- ================================================================
         SCHRITT 1b: Typ-Zuordnung
         ================================================================ -->
    <?php elseif ($step === 'type_mapping'):
        $stm = $saved_type_mapping   ?? [];
        $ssm = $saved_status_mapping ?? [];
        $hasSavedMapping = !empty($stm);
    ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-layer-group"></i> <?= $this->te('Ger&auml;tetypen zuordnen') ?></h3>
            <p class="text-muted small mb-0">
                <?= $this->te('W&auml;hlen Sie f&uuml;r jeden GLPI-Typ die entsprechende Inventar-Kategorie. Typen ohne Zuordnung werden nicht importiert.') ?>
                <?php if ($hasSavedMapping): ?>
                <span class="text-success"><i class="fas fa-check"></i> <?= $this->te('Gespeicherte Zuordnung vorausgef&uuml;llt.') ?></span>
                <?php endif; ?>
            </p>
        </div>
        <div class="card-body table-responsive p-0">
            <form method="POST" action="/admin/inventory/import/glpi/preview">
                <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">

                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col" style="width:35%;"><?= $this->te('GLPI-Typ') ?></th>
                            <th scope="col" style="width:10%;text-align:right;"><?= $this->te('Ger&auml;te') ?></th>
                            <th scope="col" style="width:55%;"><?= $this->te('Inventar-Kategorie') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($available_types as $glpiType => $count):
                        $label       = $known_type_labels[$glpiType] ?? $glpiType;
                        $savedCatId  = isset($stm[$glpiType]) ? (int)$stm[$glpiType] : 0;
                    ?>
                        <tr>
                            <td style="font-weight:500;"><?= $this->e($label) ?></td>
                            <td style="text-align:right;font-variant-numeric:tabular-nums;color:var(--color-text-muted,var(--gray-500));"><?= number_format($count, 0, ',', '.') ?></td>
                            <td>
                                <select name="type_mapping[<?= $this->e($glpiType) ?>]" class="form-control form-control-sm">
                                    <option value="0"<?= $savedCatId === 0 ? ' selected' : '' ?>><?= $this->te('&mdash; nicht importieren &mdash;') ?></option>
                                    <?php foreach ($categories as $cat): ?>
                                    <option value="<?= (int)$cat['id'] ?>"<?= $savedCatId === (int)$cat['id'] ? ' selected' : '' ?>>
                                        <?= $this->e($cat['name']) ?>
                                    </option>
                                    <?php endforeach; ?>
                                </select>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>

                <?php if (!empty($available_states)): ?>
                <div style="border-top:2px solid var(--color-border,var(--gray-200));padding:1.25rem 1.5rem 0.5rem;">
                    <h3 style="margin:0 0 0.25rem;font-size:0.95rem;font-weight:600;"><?= $this->te('Status zuordnen') ?></h3>
                    <p style="margin:0 0 1rem;font-size:0.82rem;color:var(--color-text-muted,var(--gray-500));">
                        <?= $this->te('W&auml;hlen Sie f&uuml;r jeden GLPI-Status den entsprechenden lokalen Status. Nicht zugeordnete Status verwenden den Standard-Status aus dem Formular.') ?>
                    </p>
                </div>
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col" style="width:50%;"><?= $this->te('GLPI-Status') ?></th>
                            <th scope="col" style="width:50%;"><?= $this->te('Lokaler Status') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($available_states as $stateName): ?>
                        <?php $savedStatus = $ssm[$stateName] ?? ''; ?>
                        <tr>
                            <td style="font-weight:500;"><?= $this->e($stateName) ?></td>
                            <td>
                                <select name="status_mapping[<?= $this->e($stateName) ?>]" class="form-control form-control-sm">
                                    <option value=""<?= $savedStatus === '' ? ' selected' : '' ?>><?= $this->te('Standard (aus Formular)') ?></option>
                                    <option value="active"<?= $savedStatus === 'active' ? ' selected' : '' ?>><?= $this->te('Aktiv') ?></option>
                                    <option value="storage"<?= $savedStatus === 'storage' ? ' selected' : '' ?>><?= $this->te('Lager') ?></option>
                                    <option value="decommissioned"<?= $savedStatus === 'decommissioned' ? ' selected' : '' ?>><?= $this->te('Ausgemustert') ?></option>
                                </select>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php endif; ?>

                <div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;padding:1.25rem 1.5rem;border-top:1px solid var(--color-border,var(--gray-200));">
                    <button type="submit" class="btn btn-primary"><?= $this->te('&#x1F50D; Vorschau laden') ?></button>
                    <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('&larr; Zur&uuml;ck') ?></a>
                </div>
            </form>
        </div>
    </div>

    <!-- ================================================================
         SCHRITT 2: Vorschau
         ================================================================ -->
    <?php elseif ($step === 'preview'): ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-clipboard-list"></i> <?= $this->te('Import-Vorschau') ?></h3>
            <p class="text-muted small mb-0">
                <?= $this->t('Gefunden: <strong>{anzahl}</strong> Ger&auml;te &ndash; <strong class="text-success">{neu} neu</strong>, <strong class="text-warning">{vorhanden} bereits vorhanden</strong>', ['anzahl' => count($items), 'neu' => $new_count, 'vorhanden' => $existing_count]) ?>
                <?php if ($skip_existing): ?><?= $this->te('(werden &uuml;bersprungen)') ?><?php endif; ?>
                &middot;
                <?= $this->t('Personen-Match: <strong>{treffer}</strong> von {anzahl}', ['treffer' => $person_match_count, 'anzahl' => count($items)]) ?>
            </p>
        </div>
        <div class="card-body table-responsive p-0">

            <?php if (empty($items)): ?>
                <div class="empty-state">
                    <p><?= $this->te('Keine Ger&auml;te in GLPI gefunden.') ?></p>
                </div>
            <?php else: ?>

            <div class="table-wrapper" style="border-radius:0;border:none;border-top:1px solid var(--color-border,var(--gray-200));">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col"><?= $this->te('Ger&auml;tename') ?></th>
                            <th scope="col"><?= $this->te('Inventarnummer') ?></th>
                            <th scope="col"><?= $this->te('Seriennummer') ?></th>
                            <th scope="col"><?= $this->te('Hersteller / Modell') ?></th>
                            <th scope="col"><?= $this->te('Typ') ?></th>
                            <th scope="col"><?= $this->te('Status') ?></th>
                            <th scope="col"><?= $this->te('Person (Verzeichnis)') ?></th>
                            <th scope="col"></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($items as $item): ?>
                        <tr class="<?= $item['already_exists'] ? 'exists' : '' ?>">
                            <td><?= $this->e($item['name']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?></td>
                            <td style="font-family:monospace;font-size:0.8rem;">
                                <?php if ($item['inventory_number'] !== ''): ?>
                                    <?= $this->e($item['inventory_number']) ?>
                                    <?php if ($item['inv_number_conflict']): ?>
                                        <br><span style="font-family:sans-serif;font-size:0.7rem;color:var(--amber-800);" title="<?= $this->te('Nummer bereits vergeben &ndash; wird neu generiert') ?>"><?= $this->te('&#9888; Konflikt') ?></span>
                                    <?php endif; ?>
                                <?php else: ?>
                                    <span style="color:var(--color-text-muted,var(--gray-400));font-family:sans-serif;"><?= $this->te('wird generiert') ?></span>
                                <?php endif; ?>
                            </td>
                            <td style="font-family:monospace;font-size:0.8rem;"><?= $this->e($item['serial_number']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?></td>
                            <td>
                                <?= $this->e($item['manufacturer']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?>
                                <?php if ($item['model']): ?>
                                    <br><span style="color:var(--color-text-muted,var(--gray-500));font-size:0.78rem;"><?= $this->e($item['model']) ?></span>
                                <?php endif; ?>
                            </td>
                            <td><?= match($item['glpi_type']) {
                                'Computer'         => '<span class="status-pill status-pill--info">Computer</span>',
                                'NetworkEquipment' => '<span class="status-pill status-pill--purple">Netzwerk</span>',
                                'Printer'          => '<span class="status-pill glpi-pill-printer">Drucker</span>',
                                'Phone'            => '<span class="status-pill status-pill--success">Smartphone</span>',
                                default            => '<span class="status-pill status-pill--neutral">' . $this->e($item['glpi_type']) . '</span>',
                            } ?></td>
                            <td><?php
                                $statusLabels = ['active' => 'Aktiv', 'storage' => 'Lager', 'decommissioned' => 'Ausgemustert'];
                                echo $this->e($statusLabels[$item['status']] ?? $item['status']);
                            ?></td>
                            <td><?php if ($item['person_match_id'] !== null): ?>
                                    <span style="color:var(--emerald-800);font-size:0.82rem;">&#10003; <?= $this->e($item['person_match_name']) ?></span>
                                <?php elseif ($item['glpi_user_name'] !== ''): ?>
                                    <span style="color:var(--amber-800);font-size:0.82rem;" title="<?= $this->te('GLPI: {benutzer}', ['benutzer' => $item['glpi_user_name']]) ?>">
                                        &#10007; <?= $this->e(mb_substr($item['glpi_user_name'], 0, 30)) ?>
                                    </span>
                                <?php else: ?>
                                    <span style="color:var(--color-text-muted,var(--gray-400));">&mdash;</span>
                                <?php endif; ?></td>
                            <td><?php if ($item['already_exists']): ?>
                                    <span class="status-pill status-pill--warning"><?= $this->te('Vorhanden') ?></span>
                                <?php else: ?>
                                    <span class="status-pill status-pill--success"><?= $this->te('Neu') ?></span>
                                <?php endif; ?></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>

            <div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;padding:1.25rem 1.5rem;border-top:1px solid var(--color-border,var(--gray-200));">
                <form method="POST" action="/admin/inventory/import/glpi/run" style="display:inline;">
                    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">
                    <button type="submit" class="btn btn-success">
                        &#10003; <?= $this->te($new_count === 1 ? '{anzahl} Asset importieren' : '{anzahl} Assets importieren', ['anzahl' => $new_count]) ?>
                    </button>
                </form>
                <form method="POST" action="/admin/inventory/import/glpi/dry-run" style="display:inline;">
                    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">
                    <button type="submit" class="btn btn-default" style="background:var(--violet-600);color:var(--color-on-accent);border-color:var(--violet-600);">
                        <?= $this->te('&#x1F52C; Trockenlauf') ?>
                    </button>
                </form>
                <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('&larr; Zur&uuml;ck') ?></a>
                <?php if ($skip_existing && $existing_count > 0): ?>
                    <span style="font-size:0.8rem;color:var(--color-text-muted,var(--gray-500));">
                        <?= $this->te($existing_count === 1 ? '{anzahl} vorhandenes Asset wird &uuml;bersprungen.' : '{anzahl} vorhandene Assets werden &uuml;bersprungen.', ['anzahl' => $existing_count]) ?>
                    </span>
                <?php endif; ?>
            </div>

            <?php endif; ?>
        </div>
    </div>

    <!-- ================================================================
         SCHRITT 2b: Trockenlauf-Ergebnis
         ================================================================ -->
    <?php elseif ($step === 'dryrun'): ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-vial"></i> <?= $this->te('Trockenlauf-Ergebnis') ?></h3>
            <p class="text-muted small mb-0">
                <?= $this->t('Simulation ohne Datenbank&auml;nderung &ndash; <strong class="text-success">{neu} w&uuml;rden erstellt</strong>, <strong class="text-primary">{aktualisiert} w&uuml;rden aktualisiert</strong>, <strong class="text-muted">{uebersprungen} w&uuml;rden &uuml;bersprungen</strong>.', ['neu' => $stats['create'], 'aktualisiert' => $stats['update'], 'uebersprungen' => $stats['skip']]) ?>
                <?= $this->te('Keine Daten wurden gespeichert.') ?>
            </p>
        </div>
        <div class="card-body table-responsive p-0">

            <div class="table-wrapper" style="border-radius:0;border:none;border-top:1px solid var(--color-border,var(--gray-200));">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col"><?= $this->te('Aktion') ?></th>
                            <th scope="col"><?= $this->te('Ger&auml;tename') ?></th>
                            <th scope="col"><?= $this->te('Inventarnummer') ?></th>
                            <th scope="col"><?= $this->te('Seriennummer') ?></th>
                            <th scope="col"><?= $this->te('Hersteller / Modell') ?></th>
                            <th scope="col"><?= $this->te('Typ') ?></th>
                            <th scope="col"><?= $this->te('Status') ?></th>
                            <th scope="col"><?= $this->te('Person') ?></th>
                            <th scope="col"><?= $this->te('Hinweis') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($dry_run_rows as $row):
                        $item = $row['item'];
                        $actionLabel = match($row['action']) {
                            'create' => '<span class="status-pill status-pill--success">Erstellen</span>',
                            'update' => '<span class="status-pill status-pill--info">Aktualisieren</span>',
                            'skip'   => '<span class="status-pill status-pill--neutral">&Uuml;berspringen</span>',
                            default  => $row['action'],
                        };
                    ?>
                        <tr<?= $row['action'] === 'skip' ? ' style="opacity:0.55;"' : '' ?>>
                            <td><?= $actionLabel ?></td>
                            <td><?= $this->e($item['name']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?></td>
                            <td style="font-family:monospace;font-size:0.8rem;"><?= $this->e($row['inventory_number']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?></td>
                            <td style="font-family:monospace;font-size:0.8rem;"><?= $this->e($item['serial_number']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?></td>
                            <td>
                                <?= $this->e($item['manufacturer']) ?: '<span style="color:var(--color-text-muted,var(--gray-400));">—</span>' ?>
                                <?php if ($item['model']): ?>
                                    <br><span style="color:var(--color-text-muted,var(--gray-500));font-size:0.78rem;"><?= $this->e($item['model']) ?></span>
                                <?php endif; ?>
                            </td>
                            <td><?= match($item['glpi_type']) {
                                'Computer'         => '<span class="status-pill status-pill--info">Computer</span>',
                                'NetworkEquipment' => '<span class="status-pill status-pill--purple">Netzwerk</span>',
                                'Printer'          => '<span class="status-pill glpi-pill-printer">Drucker</span>',
                                'Phone'            => '<span class="status-pill status-pill--success">Smartphone</span>',
                                default            => '<span class="status-pill status-pill--neutral">' . $this->e($item['glpi_type']) . '</span>',
                            } ?></td>
                            <td><?php
                                $statusLabels = ['active' => 'Aktiv', 'storage' => 'Lager', 'decommissioned' => 'Ausgemustert'];
                                echo $this->e($statusLabels[$item['status']] ?? $item['status']);
                            ?></td>
                            <td><?php if ($item['person_match_id'] !== null): ?>
                                    <span style="color:var(--emerald-800);font-size:0.82rem;">&#10003; <?= $this->e($item['person_match_name']) ?></span>
                                <?php elseif ($item['glpi_user_name'] !== ''): ?>
                                    <span style="color:var(--amber-800);font-size:0.82rem;" title="<?= $this->te('GLPI: {benutzer}', ['benutzer' => $item['glpi_user_name']]) ?>">
                                        &#10007; <?= $this->e(mb_substr($item['glpi_user_name'], 0, 28)) ?>
                                    </span>
                                <?php else: ?>
                                    <span style="color:var(--color-text-muted,var(--gray-400));">&mdash;</span>
                                <?php endif; ?></td>
                            <td style="font-size:0.78rem;color:var(--color-text-muted,var(--gray-500));"><?= $this->e($row['reason']) ?></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            </div>

            <div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;padding:1.25rem 1.5rem;border-top:1px solid var(--color-border,var(--gray-200));">
                <form method="POST" action="/admin/inventory/import/glpi/run" style="display:inline;">
                    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">
                    <button type="submit" class="btn btn-success">
                        &#10003; <?= $this->t('Jetzt wirklich importieren') ?> <?= $this->te('({anzahl} Assets)', ['anzahl' => $stats['create'] + $stats['update']]) ?>
                    </button>
                </form>
                <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('&larr; Zur&uuml;ck zum Formular') ?></a>
            </div>

        </div>
    </div>

    <!-- ================================================================
         SCHRITT 3: Ergebnis
         ================================================================ -->
    <?php elseif ($step === 'result'): ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-check-circle text-success"></i> <?= $this->te('Import abgeschlossen') ?></h3>
            <p class="text-muted small mb-0"><?= $this->te('{anzahl} Assets importiert, {uebersprungen} &uuml;bersprungen', ['anzahl' => $imported, 'uebersprungen' => $skipped]) ?><?= $errors > 0 ? $this->te(', {anzahl} Fehler', ['anzahl' => $errors]) : '' ?>.</p>
        </div>
        <div class="card-body">

            <div class="glpi-result-grid">
                <div class="glpi-result-card glpi-rc-imported">
                    <div class="num"><?= $imported ?></div>
                    <div class="lbl"><?= $this->te('Importiert') ?></div>
                </div>
                <div class="glpi-result-card glpi-rc-skipped">
                    <div class="num"><?= $skipped ?></div>
                    <div class="lbl"><?= $this->te('&Uuml;bersprungen') ?></div>
                </div>
                <?php if ($errors > 0): ?>
                <div class="glpi-result-card glpi-rc-errors">
                    <div class="num"><?= $errors ?></div>
                    <div class="lbl"><?= $this->te('Fehler') ?></div>
                </div>
                <?php endif; ?>
                <?php if (($location_matched ?? 0) + ($location_missing ?? 0) > 0): ?>
                <div class="glpi-result-card" style="background:var(--blue-50);color:var(--blue-800);">
                    <div class="num"><?= $location_matched ?? 0 ?></div>
                    <div class="lbl"><?= $this->te('Standorte gesetzt') ?></div>
                </div>
                <?php if (($location_missing ?? 0) > 0): ?>
                <div class="glpi-result-card" style="background:var(--amber-100);color:var(--amber-800);">
                    <div class="num"><?= $location_missing ?? 0 ?></div>
                    <div class="lbl"><?= $this->te('Standort nicht gefunden') ?></div>
                </div>
                <?php endif; ?>
                <?php endif; ?>
            </div>

            <?php if (!empty($error_messages)): ?>
            <div class="glpi-error-list">
                <strong><?= $this->te('Fehlerdetails:') ?></strong>
                <ul style="margin:0.5rem 0 0;padding-left:1.25rem;">
                    <?php foreach ($error_messages as $msg): ?>
                        <li><?= $this->e($msg) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            <?php endif; ?>

            <div class="form-actions">
                <a href="/inventory" class="btn btn-primary"><?= $this->te('Zum Inventar') ?></a>
                <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('Erneut importieren') ?></a>
            </div>

        </div>
    </div>

    <!-- ================================================================
         STANDORTE – Vorschau
         ================================================================ -->
    <?php elseif ($step === 'locations_preview'): ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-map-marker-alt"></i> <?= $this->te('Standorte aus GLPI &ndash; Vorschau') ?></h3>
            <p class="text-muted small mb-0">
                <?= $this->t('{gesamt} Standorte gefunden &ndash; <strong class="text-success">{neu} neu</strong>, <strong class="text-warning">{vorhanden} bereits vorhanden</strong> (werden nicht ver&auml;ndert). W&auml;hlen Sie, welche Standorte angelegt werden sollen.', ['gesamt' => count($location_rows), 'neu' => $new_count, 'vorhanden' => $existing_count]) ?>
            </p>
        </div>
        <div class="card-body table-responsive p-0">

            <?php if (empty($location_rows)): ?>
                <div class="empty-state">
                    <p><?= $this->te('Keine Standorte in GLPI gefunden.') ?></p>
                </div>
            <?php else: ?>

            <form method="POST" action="/admin/inventory/import/glpi/locations-run">
                <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token) ?>">

                <div class="table-wrapper" style="border-radius:0;border:none;border-top:1px solid var(--color-border,var(--gray-200));">
                    <table class="data-table">
                        <thead>
                            <tr>
                                <th scope="col" style="width:2.5rem;">
                                    <input type="checkbox" id="loc-select-all" title="<?= $this->te('Alle (de)selektieren') ?>" checked>
                                </th>
                                <th scope="col"><?= $this->te('Standort') ?></th>
                                <th scope="col"><?= $this->te('Vollst&auml;ndiger Pfad') ?></th>
                                <th scope="col"></th>
                            </tr>
                        </thead>
                        <tbody>
                        <?php foreach ($location_rows as $row): ?>
                            <tr<?= $row['already_exists'] ? ' style="opacity:0.55;"' : '' ?>>
                                <td>
                                    <?php if (!$row['already_exists']): ?>
                                    <input type="checkbox" name="selected_locations[]"
                                           value="<?= (int)$row['glpi_id'] ?>"
                                           class="loc-checkbox" checked>
                                    <?php endif; ?>
                                </td>
                                <td style="padding-left: <?= 0.85 + $row['depth'] * 1.5 ?>rem; font-weight: <?= $row['depth'] === 0 ? '600' : '400' ?>;">
                                    <?php if ($row['depth'] > 0): ?>
                                        <span style="color:var(--color-text-muted,var(--gray-400));margin-right:0.25rem;"><?= str_repeat('└ ', 1) ?></span>
                                    <?php endif; ?>
                                    <?= $this->e($row['name']) ?>
                                </td>
                                <td style="color:var(--color-text-muted,var(--gray-500));font-size:0.78rem;">
                                    <?= $this->e($row['completename']) ?>
                                </td>
                                <td>
                                    <?php if ($row['already_exists']): ?>
                                        <span class="status-pill status-pill--warning"><?= $this->te('Vorhanden') ?></span>
                                    <?php else: ?>
                                        <span class="status-pill status-pill--success"><?= $this->te('Neu') ?></span>
                                    <?php endif; ?>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>

                <div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;padding:1.25rem 1.5rem;border-top:1px solid var(--color-border,var(--gray-200));">
                    <button type="submit" class="btn btn-success">
                        &#x1F4CD; <?= $this->te($new_count === 1 ? '{anzahl} Standort anlegen' : '{anzahl} Standorte anlegen', ['anzahl' => $new_count]) ?>
                    </button>
                    <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('&larr; Zur&uuml;ck') ?></a>
                    <span style="font-size:0.8rem;color:var(--color-text-muted,var(--gray-500));">
                        <?= $this->te('Bereits vorhandene Standorte werden nicht ver&auml;ndert. Eltern-Standorte m&uuml;ssen mit ihren Kindern zusammen ausgew&auml;hlt werden.') ?>
                    </span>
                </div>

            </form>

            <?php endif; ?>
        </div>
    </div>

    <!-- ================================================================
         STANDORTE – Ergebnis
         ================================================================ -->
    <?php elseif ($step === 'locations_result'): ?>

    <div class="card card-outline card-primary mb-3">
        <div class="card-header">
            <h3 class="card-title"><i class="fas fa-map-marker-alt text-success"></i> <?= $this->te('Standorte importiert') ?></h3>
            <p class="text-muted small mb-0">
                <?= $this->te($imported === 1 ? '{anzahl} Standort angelegt' : '{anzahl} Standorte angelegt', ['anzahl' => $imported]) ?>,
                <?= $this->te('{anzahl} &uuml;bersprungen', ['anzahl' => $skipped]) ?><?= $errors > 0 ? $this->te(', {anzahl} Fehler', ['anzahl' => $errors]) : '' ?>.
            </p>
        </div>
        <div class="card-body">

            <div class="glpi-result-grid">
                <div class="glpi-result-card glpi-rc-imported">
                    <div class="num"><?= $imported ?></div>
                    <div class="lbl"><?= $this->te('Angelegt') ?></div>
                </div>
                <div class="glpi-result-card glpi-rc-skipped">
                    <div class="num"><?= $skipped ?></div>
                    <div class="lbl"><?= $this->te('&Uuml;bersprungen') ?></div>
                </div>
                <?php if ($errors > 0): ?>
                <div class="glpi-result-card glpi-rc-errors">
                    <div class="num"><?= $errors ?></div>
                    <div class="lbl"><?= $this->te('Fehler') ?></div>
                </div>
                <?php endif; ?>
            </div>

            <?php if (!empty($error_messages)): ?>
            <div class="glpi-error-list">
                <strong><?= $this->te('Fehlerdetails:') ?></strong>
                <ul style="margin:0.5rem 0 0;padding-left:1.25rem;">
                    <?php foreach ($error_messages as $msg): ?>
                        <li><?= $this->e($msg) ?></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            <?php endif; ?>

            <div class="form-actions">
                <a href="/admin/locations" class="btn btn-primary"><?= $this->te('Zu den Standorten') ?></a>
                <a href="/admin/inventory/import/glpi" class="btn btn-default"><?= $this->te('Assets importieren') ?></a>
            </div>

        </div>
    </div>

    <?php endif; ?>


<script>
function toggleAuth(value) {
    document.getElementById('auth-token').style.display        = value === 'token'       ? '' : 'none';
    document.getElementById('auth-credentials').style.display  = value === 'credentials' ? '' : 'none';
}
(function () {
    var checked = document.querySelector('input[name="auth_method"]:checked');
    if (checked) toggleAuth(checked.value);

    var selectAll = document.getElementById('loc-select-all');
    if (selectAll) {
        selectAll.addEventListener('change', function () {
            document.querySelectorAll('.loc-checkbox').forEach(function (cb) {
                cb.checked = selectAll.checked;
            });
        });
    }
})();
</script>